天天看點

Go 語言的 10 個實用技巧

原文連結

使用單一的 GOPATH

多個 GOPATH 的情況并不具有彈性。GOPATH 本身就是高度自我完備的(通過導入路徑)。有多個 GOPATH 會導緻某些副作用,例如可能使用了給定的庫的不同的版本。你可能在某個地方更新了它,但是其他地方卻沒有更新。而且,我還沒遇到過任何一個需要使用多個 GOPATH 的情況。是以隻使用單一的 GOPATH,這會提升你 Go 的開發進度。

許多人不同意這一觀點,接下來我會做一些澄清。像 etcd 或camlistore 這樣的大項目使用了像 godep 這樣的工具,将所有依賴儲存到某個目錄中。也就是說,這些項目自身有一個單一的 GOPATH。它們隻能在這個目錄裡找到對應的版本。除非你的項目很大并且極為重要,否則不要為每個項目使用不同的 GOPATH。如果你認為項目需要一個自己的 GOPATH 目錄,那麼就建立它,否則不要嘗試使用多個 GOPATH。它隻會拖慢你的進度。

将 for-select 封裝到函數中

如果在某個條件下,你需要從 for-select 中退出,就需要使用标簽。例如:

func main() {

L:
for {
select {
case <-time.After(time.Second):
fmt.Println(“hello”)
default:
break L
}
}

fmt.Println(“ending”)
}
           

如你所見,需要聯合break使用标簽。這有其用途,不過我不喜歡。這個例子中的 for 循環看起來很小,但是通常它們會更大,而判斷break的條件也更為冗長。

如果需要退出循環,我會将 for-select 封裝到函數中:

func main() {
foo()
fmt.Println(“ending”)
}

func foo() {
for {
select {
case <-time.After(time.Second):
fmt.Println(“hello”)
default:
return
}
}
}
           

你還可以傳回一個錯誤(或任何其他值),也是同樣漂亮的,隻需要:

// 阻塞
if err := foo(); err != nil {
// 處理 err
}
           

在初始化結構體時使用帶有标簽的文法

這是一個無标簽文法的例子:

type T struct {
Foo string
Bar int
}

func main() {
t := T{“example”, 123} // 無标簽文法
fmt.Printf(“t %+v\n”, t)
}
           

那麼如果你添加一個新的字段到T結構體,代碼會編譯失敗:

type T struct {
Foo string
Bar int
Qux string
}

func main() {
t := T{“example”, 123} // 無法編譯
fmt.Printf(“t %+v\n”, t)
}
           

如果使用了标簽文法,Go 的相容性規則(

http://golang.org/doc/go1compat

)會處理代碼。例如在向net包的類型添加叫做Zone的字段,參見:

http://golang.org/doc/go1.1#library

。回到我們的例子,使用标簽文法:

type T struct {
Foo string
Bar int
Qux string
}

func main() {
t := T{Foo: “example”, Qux: 123}
fmt.Printf(“t %+v\n”, t)
}
           

這個編譯起來沒問題,而且彈性也好。不論你如何添加其他字段到T結構體。你的代碼總是能編譯,并且在以後的 Go 的版本也可以保證這一點。隻要在代碼集中執行go vet,就可以發現所有的無标簽的文法。

将結構體的初始化拆分到多行

如果有兩個以上的字段,那麼就用多行。它會讓你的代碼更加容易閱讀,也就是說不要:

T{Foo: “example”, Bar:someLongVariable, Qux:anotherLongVariable, B: forgetToAddThisToo}
           

而是:

T{
Foo: “example”,
Bar: someLongVariable,
Qux: anotherLongVariable,
B: forgetToAddThisToo,
}
           

這有許多好處,首先它容易閱讀,其次它使得允許或屏蔽字段初始化變得容易(隻要注釋或删除它們),最後添加其他字段也更容易(隻要添加一行)。

為整數常量添加 String() 方法

如果你利用 iota 來使用自定義的整數枚舉類型,務必要為其添加 String() 方法。例如,像這樣:

type State int

const (
Running State = iota
Stopped
Rebooting
Terminated
)
           

如果你建立了這個類型的一個變量,然後輸出,會得到一個整數(

http://play.golang.org/p/V5VVFB05HB

):

func main() {
state := Running

// print: “state 0″
fmt.Println(“state “, state)
}
           

除非你回顧常量定義,否則這裡的0看起來毫無意義。隻需要為State類型添加String()方法就可以修複這個問題(

http://play.golang.org/p/ewMKl6K302
func (s State) String() string {
switch s {
case Running:
return “Running”
case Stopped:
return “Stopped”
case Rebooting:
return “Rebooting”
case Terminated:
return “Terminated”
default:
return “Unknown”
}
}
           

新的輸出是:state: Running。顯然現在看起來可讀性好了很多。在你調試程式的時候,這會帶來更多的便利。同時還可以在實作 MarshalJSON()、UnmarshalJSON() 這類方法的時候使用同樣的手段。

讓 iota 從 a +1 開始增量

在前面的例子中同時也産生了一個我已經遇到過許多次的 bug。假設你有一個新的結構體,有一個State字段:

type T struct {
Name  string
Port  int
State State
}
現```  
在如果基于 T 建立一個新的變量,然後輸出,你會得到奇怪的結果(http://play.golang.org/p/LPG2RF3y39):
           

func main() {

t := T{Name: “example”, Port: 6666}

// prints: “t {Name:example Port:6666 State:Running}”

fmt.Printf(“t %+vn”, t)

}

看到 bug 了嗎?State字段沒有初始化,Go 預設使用對應類型的零值進行填充。由于State是一個整數,零值也就是0,但在我們的例子中它表示Running。

那麼如何知道 State 被初始化了?還是它真得是在Running模式?沒有辦法區分它們,那麼這就會産生未知的、不可預測的 bug。不過,修複這個很容易,隻要讓 iota 從 +1 開始(http://play.golang.org/p/VyAq-3OItv):
           

const (

Running State = iota + 1

Stopped

Rebooting

Terminated

)

現在t變量将預設輸出Unknown,不是嗎? :
           

// 輸出: “t {Name:example Port:6666 State:Unknown}”

不過讓 iota 從零值開始也是一種解決辦法。例如,你可以引入一個新的狀态叫做Unknown,将其修改為:
           

Unknown State = iota

Running

`

回函數調用

我已經看過很多代碼例如(

http://play.golang.org/p/8Rz1EJwFTZ
func bar() (string, error) {
v, err := foo()
if err != nil {
return “”, err
}

return v, nil
}
然```  
而,你隻需要:
           

func bar() (string, error) {

return foo()

更簡單也更容易閱讀(當然,除非你要對某些内部的值做一些記錄)。

把 slice、map 等定義為自定義類型
将 slice 或 map 定義成自定義類型可以讓代碼維護起來更加容易。假設有一個Server類型和一個傳回伺服器清單的函數:
           

type Server struct {

Name string

func ListServers() []Server {

return []Server{

{Name: “Server1″},

{Name: “Server2″},

{Name: “Foo1″},

{Name: “Foo2″},

`

在假設需要擷取某些特定名字的伺服器。需要對 ListServers() 做一些改動,增加篩選條件:

// ListServers 傳回伺服器清單。隻會傳回包含 name 的伺服器。空的 name 将會傳回所有伺服器。
func ListServers(name string) []Server {
servers := []Server{
{Name: “Server1″},
{Name: “Server2″},
{Name: “Foo1″},
{Name: “Foo2″},
}

// 傳回所有伺服器
if name == “” {
return servers
}

// 傳回過濾後的結果
filtered := make([]Server, 0)

for _, server := range servers {
if strings.Contains(server.Name, name) {
filtered = append(filtered, server)
}
}

return filtered
}           

現在可以用這個來篩選有字元串Foo的伺服器:

func main() {
servers := ListServers(“Foo”)

// 輸出:“servers [{Name:Foo1} {Name:Foo2}]”
fmt.Printf(“servers %+v\n”, servers)
}
           

顯然這個函數能夠正常工作。不過它的彈性并不好。如果你想對伺服器集合引入其他邏輯的話會如何呢?例如檢查所有伺服器的狀态,為每個伺服器建立一個資料庫記錄,用其他字段進行篩選等等……

現在引入一個叫做Servers的新類型,并且修改原始版本的 ListServers() 傳回這個新類型:

type Servers []Server

// ListServers 傳回伺服器清單
func ListServers() Servers {
return []Server{
{Name: “Server1″},
{Name: “Server2″},
{Name: “Foo1″},
{Name: “Foo2″},
}
}
           

現在需要做的是隻要為Servers類型添加一個新的Filter()方法:

// Filter 傳回包含 name 的伺服器。空的 name 将會傳回所有伺服器。
func (s Servers) Filter(name string) Servers {
filtered := make(Servers, 0)

for _, server := range s {
if strings.Contains(server.Name, name) {
filtered = append(filtered, server)
}

}

return filtered
}
           

現在可以針對字元串Foo篩選伺服器:

func main() {
servers := ListServers()
servers = servers.Filter(“Foo”)
fmt.Printf(“servers %+v\n”, servers)
}
           

哈!看到你的代碼是多麼的簡單了嗎?還想對伺服器的狀态進行檢查?或者為每個伺服器添加一條資料庫記錄?沒問題,添加以下新方法即可:

func (s Servers) Check()
func (s Servers) AddRecord()
func (s Servers) Len()
…
           

withContext 封裝函數

有時對于函數會有一些重複勞動,例如鎖/解鎖,初始化一個新的局部上下文,準備初始化變量等等……這裡有一個例子:

func foo() {
mu.Lock()
defer mu.Unlock()

// foo 相關的工作
}

func bar() {
mu.Lock()
defer mu.Unlock()

// bar 相關的工作
}

func qux() {
mu.Lock()
defer mu.Unlock()

// qux 相關的工作
}
           

如果你想要修改某個内容,你需要對所有的都進行修改。如果它是一個常見的任務,那麼最好建立一個叫做withContext的函數。這個函數的輸入參數是另一個函數,并用調用者提供的上下文來調用它:

func withLockContext(fn func()) {
mu.Lock
defer mu.Unlock()

fn()
}
           

隻需要将之前的函數用這個進行封裝:

func foo() {
withLockContext(func() {
// foo 相關工作
})
}

func bar() {
withLockContext(func() {
// bar 相關工作
})
}

func qux() {
withLockContext(func() {
// qux 相關工作
})
}
           

不要光想着加鎖的情形。對此來說最好的用例是資料庫連結。現在對 withContext 函數作一些小小的改動:

func withDBContext(fn func(db DB) error) error {
// 從連接配接池擷取一個資料庫連接配接
dbConn := NewDB()

return fn(dbConn)
}
           

如你所見,它擷取一個連接配接,然後傳遞給提供的參數,并且在調用函數的時候傳回錯誤。你需要做的隻是:

func foo() {
withDBContext(func(db *DB) error {
// foo 相關工作
})
}

func bar() {
withDBContext(func(db *DB) error {
// bar 相關工作
})
}

func qux() {
withDBContext(func(db *DB) error {
// qux 相關工作
})
}
           

你在考慮一個不同的場景,例如作一些預初始化?沒問題,隻需要将它們加到withDBContext就可以了。這對于測試也同樣有效。

這個方法有個缺陷,它增加了縮進并且更難閱讀。再次提示,永遠尋找最簡單的解決方案。

為通路 map 增加 setter,getters

如果你重度使用 map 讀寫資料,那麼就為其添加 getter 和 setter 吧。通過 getter 和 setter 你可以将邏輯封分别裝到函數裡。這裡最常見的錯誤就是并發通路。如果你在某個 goroutein 裡有這樣的代碼:

m["foo"] = bar
           

還有這個:

delete(m, “foo”)
           

會發生什麼?你們中的大多數應當已經非常熟悉這樣的競态了。簡單來說這個競态是由于 map 預設并非線程安全。不過你可以用互斥量來保護它們:

mu.Lock()
m["foo"] = “bar”
mu.Unlock()
以及:

mu.Lock()
delete(m, “foo”)
mu.Unlock()
           

假設你在其他地方也使用這個 map。你必須把互斥量放得到處都是!然而通過 getter 和 setter 函數就可以很容易的避免這個問題:

func Put(key, value string) {
mu.Lock()
m[key] = value
mu.Unlock()
}
func Delete(key string) {
mu.Lock()
delete(m, key)
mu.Unlock()
}
           

使用接口可以對這一過程做進一步的改進。你可以将實作完全隐藏起來。隻使用一個簡單的、設計良好的接口,然後讓包的使用者使用它們:

type Storage interface {
Delete(key string)
Get(key string) string
Put(key, value string)
}
           

這隻是個例子,不過你應該能體會到。對于底層的實作使用什麼都沒關系。不光是使用接口本身很簡單,而且還解決了暴露内部資料結構帶來的大量的問題。

但是得承認,有時隻是為了同時對若幹個變量加鎖就使用接口會有些過分。了解你的程式,并且在你需要的時候使用這些改進。 總結

抽象永遠都不是容易的事情。有時,最簡單的就是你已經實作的方法。要知道,不要讓你的代碼看起來很聰明。Go 天生就是個簡單的語言,在大多數情況下隻會有一種方法來作某事。簡單是力量的源泉,也是為什麼在人的層面它表現的如此有彈性。

如果必要的話,使用這些基數。例如将[]Server轉化為Servers是另一種抽象,僅在你有一個合理的理由的情況下這麼做。不過有一些技術,如 iota 從 1 開始計數總是有用的。再次提醒,永遠保持簡單。

掃碼進群,阿裡雲等各界大佬教你更多Go語言編碼小技巧

Go 語言的 10 個實用技巧