這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
原文在此,實用總結。
————翻譯分隔線————
十條有用的 Go 技術
這裡是我過去幾年中編寫的大量 Go 代碼的經驗總結而來的自己的最佳實務。我相信它們具有彈性的。這裡的彈性是指:
某個應用需要適配一個靈活的環境。你不希望每過 3 到 4 個月就不得不將它們全部重構一遍。添加新的特性應當很容易。許多人蔘與開發該應用,它應當可以被理解,且維護簡單。許多人使用該應用,bug 應該容易被發現並且可以快速的修複。我用了很長的時間學到了這些事情。其中的一些很微小,但對於許多事情都會有影響。所有這些都僅僅是建議,具體情況具體對待,並且如果有協助的話務必告訴我。隨時留言:)
1. 使用單一的 GOPATH
多個 GOPATH 的情況並不具有彈性。GOPATH 本身就是高度自我完備的(通過匯入路徑)。有多個 GOPATH 會導致某些副作用,例如可能使用了給定的庫的不同的版本。你可能在某個地方升級了它,但是其他地方卻沒有升級。而且,我還沒遇到過任何一個需要使用多個 GOPATH 的情況。所以只使用單一的 GOPATH,這會提升你 Go 的開發進度。
許多人不同意這一觀點,接下來我會做一些澄清。像 etcd 或 camlistore 這樣的大項目使用了像 godep 這樣的工具,將所有依賴儲存到某個目錄中。也就是說,這些項目自身有一個單一的 GOPATH。它們只能在這個目錄裡找到對應的版本。除非你的項目很大並且極為重要,否則不要為每個項目使用不同的 GOPATH。如果你認為項目需要一個自己的 GOPATH 目錄,那麼就建立它,否則不要嘗試使用多個 GOPATH。它只會拖慢你的進度。
2. 將 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}
3. 在初始化結構體時使用帶有標籤的文法
這是一個無標籤文法的例子:
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,就可以發現所有的無標籤的文法。
4. 將結構體的初始化拆分到多行
如果有兩個以上的欄位,那麼就用多行。它會讓你的代碼更加容易閱讀,也就是說不要:
T{Foo: "example", Bar:someLongVariable, Qux:anotherLongVariable, B: forgetToAddThisToo}
而是:
T{ Foo: "example", Bar: someLongVariable, Qux: anotherLongVariable, B: forgetToAddThisToo,}
這有許多好處,首先它容易閱讀,其次它使得允許或屏蔽欄位初始化變得容易(只要注釋或刪除它們),最後添加其他欄位也更容易(只要添加一行)。
5. 為整數常量添加 String() 方法
如果你利用 iota 來使用自訂的整數枚舉類型,務必要為其添加 String() 方法。例如,像這樣:
type State intconst ( 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() 這類方法的時候使用同樣的手段。
6. 讓 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 %+v\n", 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,不是嗎?