十條有用的 GO 技術(shù)

這是我這幾年寫(xiě) Go 代碼的一些經(jīng)驗(yàn)總結(jié)。我相信他們?cè)谝恍┣闆r下能幫助到你們。例如:
你開(kāi)發(fā)的應(yīng)用依賴(lài)經(jīng)常改變。你不希望每 3 - 4 個(gè)月就不能不重構(gòu)這些代碼。新的功能應(yīng)該很輕易的添加上去。
你的應(yīng)用由多人合作開(kāi)發(fā),它代碼應(yīng)該易讀和方便維護(hù)的。
你的應(yīng)用被很多人使用,有一些很容易發(fā)現(xiàn)并需要快速修復(fù)的 bug。
隨著時(shí)間的推移,我發(fā)現(xiàn)這些事情不管在什么時(shí)候都是很重要的。有一些雖然比較次要,但他們影響著很多事情。下面是一些建議,如果它們能夠在工作上幫到你,請(qǐng)讓我知道。請(qǐng)隨意發(fā)表你的看法 :)
0.0.1 1. 使用單一的 GOPATH
多個(gè) GOPATH 的情況并不具有彈性。GOPATH 本身就是高度自我完備的(通過(guò)導(dǎo)入路徑)。有多個(gè) GOPATH 會(huì)導(dǎo)致某些副作用,例如可能使用了給定的庫(kù)的不同的版本。你可能在某個(gè)地方升級(jí)了它,但是其他地方卻沒(méi)有升級(jí)。而且,我還沒(méi)遇到過(guò)任何一個(gè)需要使用多個(gè) GOPATH 的情況。所以只使用單一的 GOPATH,這會(huì)提升你 Go 的開(kāi)發(fā)進(jìn)度。
許多人不同意這一觀點(diǎn),接下來(lái)我會(huì)做一些澄清。像 [etcd](https: //github.com/coreos/etcd) 或 [camlistore](https: //camlistore.org/) 這樣的大項(xiàng)目使用了像 [godep](https: //github.com/tools/godep) 這樣的工具,將所有依賴(lài)保存到某個(gè)目錄中。也就是說(shuō),這些項(xiàng)目自身有一個(gè)單一的 GOPATH。它們只能在這個(gè)目錄里找到對(duì)應(yīng)的版本。除非你的項(xiàng)目很大并且極為重要,否則不要為每個(gè)項(xiàng)目使用不同的 GOPATH。如果你認(rèn)為項(xiàng)目需要一個(gè)自己的 GOPATH 目錄,那么就創(chuàng)建它,否則不要嘗試使用多個(gè) GOPATH。它只會(huì)拖慢你的進(jìn)度。
0.0.2 2. 將 「for-select」 語(yǔ)法結(jié)構(gòu)封裝成函數(shù)
如果需要中斷 「for-select」 語(yǔ)法結(jié)構(gòu),通常是需要使用標(biāo)簽來(lái)實(shí)現(xiàn)的,示例如下:
func main() {
L:
for {
select {
case <-time.After(time.Second):
fmt.Println("hello")
default:
break L
}
}
fmt.Println("ending")
}
如你所見(jiàn),在 「for-select」 語(yǔ)法結(jié)構(gòu)中,需要結(jié)合標(biāo)簽的使用來(lái)實(shí)現(xiàn)中斷。這是一種常見(jiàn)的用法,但是我并不喜歡。因?yàn)槭纠械难h(huán)結(jié)構(gòu)看起來(lái)很簡(jiǎn)短明了,這樣使用并不顯得有太大的問(wèn)題,但在實(shí)際中通常要比這個(gè)復(fù)雜得多,并且追蹤中斷情形會(huì)顯得十分冗長(zhǎng)繁瑣。
因此如果我需要在 「for-select」 語(yǔ)法結(jié)構(gòu)中實(shí)現(xiàn)中斷,我通常會(huì)將其封裝成一個(gè)函數(shù)來(lái)實(shí)現(xiàn):
func main() {
foo()
fmt.Println("ending")
}
func foo() {
for {
select {
case <-time.After(time.Second):
fmt.Println("hello")
default:
return
}
}
}
這就簡(jiǎn)潔優(yōu)雅多了,當(dāng)然你也可以在這個(gè)函數(shù)中返回錯(cuò)誤(或任意其它值)來(lái)幫助函數(shù)調(diào)用者完善業(yè)務(wù)邏輯的處理,例如這樣:
// blocking
if err := foo(); err != nil {
// 處理錯(cuò)誤
}
0.0.3 3. 在初始化結(jié)構(gòu)體時(shí)使用帶有標(biāo)簽的語(yǔ)法
這是一個(gè)無(wú)標(biāo)簽語(yǔ)法的例子:
type T struct {
Foo string
Bar int
}
func main() {
t := T{"example", 123} // 無(wú)標(biāo)簽語(yǔ)法
fmt.Printf("t %+vn", t)
}
那么如果你添加一個(gè)新的字段到T結(jié)構(gòu)體,代碼會(huì)編譯失?。?/p>
type T struct {
Foo string
Bar int
Qux string
}
func main() {
t := T{"example", 123} // 無(wú)法編譯
fmt.Printf("t %+vn", t)
}
如果使用了標(biāo)簽語(yǔ)法,Go 的兼容性規(guī)則 (http://golang.org/doc/go1compat) 會(huì)處理代碼。例如在向 net 包的類(lèi)型添加叫做 Zone 的字段,參見(jiàn):http://golang.org/doc/go1.1#library?;氐轿覀兊睦?,使用標(biāo)簽語(yǔ)法:
type T struct {
Foo string
Bar int
Qux string
}
func main() {
t := T{Foo: "example", Bar: 123}
fmt.Printf("t %+vn", t)
}
這個(gè)編譯起來(lái)沒(méi)問(wèn)題,而且彈性也好。不論你如何添加其他字段到T結(jié)構(gòu)體。你的代碼總是能編譯,并且在以后的 Go 的版本也可以保證這一點(diǎn)。只要在代碼集中執(zhí)行 go vet,就可以發(fā)現(xiàn)所有的無(wú)標(biāo)簽的語(yǔ)法。
0.0.4 4. 將結(jié)構(gòu)體 「struct」 的初始化拆分成多行
如果你要初始化的結(jié)構(gòu)體多于2個(gè)字段,請(qǐng)你務(wù)必拆分成多行來(lái)進(jìn)行。此舉會(huì)使你的代碼更簡(jiǎn)單明了、易于閱讀,而非如下所示讓人難以閱讀:
T{Foo: "example", Bar:someLongVariable, Qux:anotherLongVariable, B: forgetToAddThisToo}
以上例子的初始化應(yīng)當(dāng)這樣做:
T{
Foo: "example",
Bar: someLongVariable,
Qux: anotherLongVariable,
B: forgetToAddThisToo,
}
這樣做會(huì)有以下幾個(gè)優(yōu)點(diǎn):
清晰明了,易于閱讀;
啟/禁用某些字段初始化更簡(jiǎn)單(只需注釋或刪除對(duì)應(yīng)行即可);
添加新的字段初始化更簡(jiǎn)單(只需新增一行即可)。
0.0.5 5. 為整型常量值增加一個(gè) String() 方法
如果你使用 iota 的自定義整型用于枚舉,請(qǐng)始終增加一個(gè) String() 方法。假設(shè)你寫(xiě)了這樣的代碼:
type State int
const (
Running State = iota
Stopped
Rebooting
Terminated
)
如果你為 State 類(lèi)型賦值,并且輸出它,那么你將會(huì)看到一個(gè)數(shù)字 (http://play.golang.org/p/V5VVFB05HB):
func main() {
state := Running
// print: "state 0"
fmt.Println("state ", state)
}
在這里0是沒(méi)有任何意義的,除非你去看回你聲明變量的代碼。如果你為你的 State 類(lèi)型增加一個(gè) 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. 正如你所見(jiàn),這種輸出具有可讀性。如果你需要調(diào)試你的應(yīng)用 ,這將會(huì)使你的工作更加輕松。你也可以通過(guò)實(shí)現(xiàn) MarshalJSON(), UnmarshalJSON() 等等方法,達(dá)到同樣的效果。
在最后, 其實(shí)這些都可以通過(guò) Stringer 工具去自動(dòng)化實(shí)現(xiàn):
[https: //godoc.org/golang.org/x/tools/cmd/s...](https: //godoc.org/golang.org/x/tools/cmd/stringer)
這個(gè)工具通過(guò) go generate 去自動(dòng)化生成基于整型的高效 String 方法
0.0.6 6. 讓 iota 從 a +1 開(kāi)始增量
在前面的例子中同時(shí)也產(chǎn)生了一個(gè)我已經(jīng)遇到過(guò)許多次的 bug。假設(shè)你有一個(gè)新的結(jié)構(gòu)體,有一個(gè) State 字段:
type T struct {
Name string
Port int
State State
}
現(xiàn)在如果基于 T 創(chuàng)建一個(gè)新的變量,然后輸出,你會(huì)得到奇怪的結(jié)果(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 字段沒(méi)有被初始化,Go 默認(rèn)使用對(duì)應(yīng)類(lèi)型的零值進(jìn)行填充。由于 State 是一個(gè)整數(shù),零值也就是 0,但在我們的例子中它表示 Running。
如何知道 State 被初始化了?如何得知是在 Running 模式?事實(shí)上,沒(méi)有很好的辦法區(qū)分它們,并且它們還會(huì)產(chǎn)生更多未知的、不可預(yù)測(cè)的 Bug。不過(guò),修復(fù)這個(gè)很容易,只要讓 iota 從 +1 開(kāi)始 (http://play.golang.org/p/VyAq-3OItv):
const (
Running State = iota + 1
Stopped
Rebooting
Terminated
)
現(xiàn)在 t 變量將默認(rèn)輸出 Unknown,是不是?:) :
func main() {
t := T{Name: "example", Port: 6666}
// 輸出: "t {Name:example Port:6666 State:Unknown}"
fmt.Printf("t %+vn", t)
}
不過(guò)讓 iota 從零值開(kāi)始也是一種解決辦法。例如,你可以引入一個(gè)新的狀態(tài)叫做 Unknown,將其修改為:
const (
Unknown State = iota
Running
Stopped
Rebooting
Terminated
)
0.0.7 7. 返回回調(diào)函數(shù)
我看過(guò)很多代碼像 (http://play.golang.org/p/8Rz1EJwFTZ):
func bar() (string, error) {
v, err := foo()
if err != nil {
return "", err
}
return v, nil
}
然而你可以這樣寫(xiě):
func bar() (string, error) {
return foo()
}
簡(jiǎn)單易讀 (除非你想記錄一些中間值)。
0.0.8 8. 把 slice、map 等定義為自定義類(lèi)型
將 slice 或 map 定義成自定義類(lèi)型可以讓你的代碼更容易維護(hù)。假設(shè)有一個(gè) Server 類(lèi)型和一個(gè)返回服務(wù)器列表的函數(shù):
type Server struct {
Name string
}
func ListServers() []Server {
return []Server{
{Name: "Server1"},
{Name: "Server2"},
{Name: "Foo1"},
{Name: "Foo2"},
}
}
現(xiàn)在假設(shè)需要獲取某些特定名字的服務(wù)器。需要對(duì) ListServers() 做一些改動(dòng),增加篩選條件:
// ListServers 返回服務(wù)器列表。只會(huì)返回包含 name 的服務(wù)器。空的 name 將會(huì)返回所有服務(wù)器。
func ListServers(name string) []Server {
servers := []Server{
{Name: "Server1"},
{Name: "Server2"},
{Name: "Foo1"},
{Name: "Foo2"},
}
// 返回所有服務(wù)器
if name == "" {
return servers
}
// 返回過(guò)濾后的結(jié)果
filtered := make([]Server, 0)
for _, server := range servers {
if strings.Contains(server.Name, name) {
filtered = append(filtered, server)
}
}
return filtered
}
現(xiàn)在可以用這個(gè)來(lái)篩選有字符串 Foo 的服務(wù)器:
func main() {
servers := ListServers("Foo")
// 輸出: "servers [{Name:Foo1} {Name:Foo2}]"
fmt.Printf("servers %+vn", servers)
}
顯然這個(gè)函數(shù)能夠正常工作。不過(guò)它的彈性并不好。如果你想對(duì)服務(wù)器集合引入其他邏輯的話會(huì)如何呢?例如檢查所有服務(wù)器的狀態(tài),為每個(gè)服務(wù)器創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)記錄,用其他字段進(jìn)行篩選等等……
現(xiàn)在引入一個(gè)叫做 Servers 的新類(lèi)型,并且修改原始版本的 ListServers() 返回這個(gè)新類(lèi)型:
type Servers []Server
// ListServers 返回服務(wù)器列表
func ListServers() Servers {
return []Server{
{Name: "Server1"},
{Name: "Server2"},
{Name: "Foo1"},
{Name: "Foo2"},
}
}
現(xiàn)在需要做的是只要為 Servers 類(lèi)型添加一個(gè)新的 Filter() 方法:
// Filter 返回包含 name 的服務(wù)器??盏?name 將會(huì)返回所有服務(wù)器。
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
}
現(xiàn)在可以針對(duì)字符串 Foo 篩選服務(wù)器:
func main() {
servers := ListServers()
servers = servers.Filter("Foo")
fmt.Printf("servers %+vn", servers)
}
哈!看到你的代碼是多么的簡(jiǎn)單了嗎?還想對(duì)服務(wù)器的狀態(tài)進(jìn)行檢查?或者為每個(gè)服務(wù)器添加一條數(shù)據(jù)庫(kù)記錄?沒(méi)問(wèn)題,添加以下新方法即可:
func (s Servers) Check()
func (s Servers) AddRecord()
func (s Servers) Len()
...
0.0.9 9. withContext 封裝函數(shù)
有時(shí)對(duì)于函數(shù)會(huì)有一些重復(fù)勞動(dòng),例如鎖/解鎖,初始化一個(gè)新的局部上下文,準(zhǔn)備初始化變量等等……這里有一個(gè)例子:
func foo() {
mu.Lock()
defer mu.Unlock()
// foo 相關(guān)的工作
}
func bar() {
mu.Lock()
defer mu.Unlock()
// bar 相關(guān)的工作
}
func qux() {
mu.Lock()
defer mu.Unlock()
// qux 相關(guān)的工作
}
如果你想要修改某個(gè)內(nèi)容,你需要對(duì)所有的都進(jìn)行修改。如果它是一個(gè)常見(jiàn)的任務(wù),那么最好創(chuàng)建一個(gè)叫做 withContext 的函數(shù)。這個(gè)函數(shù)的輸入?yún)?shù)是另一個(gè)函數(shù),并用調(diào)用者提供的上下文來(lái)調(diào)用它:
func withLockContext(fn func()) {
mu.Lock
defer mu.Unlock()
fn()
}
只需要將之前的函數(shù)用這個(gè)進(jìn)行封裝:
func foo() {
withLockContext(func() {
// foo 相關(guān)的工作
})
}
func bar() {
withLockContext(func() {
// bar 相關(guān)的工作
})
}
func qux() {
withLockContext(func() {
// qux 相關(guān)的工作
})
}
不要光想著加鎖的情形。對(duì)此來(lái)說(shuō)最好的用例是數(shù)據(jù)庫(kù)鏈接?,F(xiàn)在對(duì) withContext 函數(shù)作一些小小的改動(dòng):
func withDBContext(fn func(db DB)) error {
// 從連接池獲取一個(gè)數(shù)據(jù)庫(kù)連接
dbConn := NewDB()
return fn(dbConn)
}
如你所見(jiàn),它獲取一個(gè)連接,然后傳遞給提供的參數(shù),并且在調(diào)用函數(shù)的時(shí)候返回錯(cuò)誤。你需要做的只是:
func foo() {
withDBContext(func(db *DB) error {
// foo 相關(guān)的工作
})
}
func bar() {
withDBContext(func(db *DB) error {
// bar 相關(guān)的工作
})
}
func qux() {
withDBContext(func(db *DB) error {
// qux 相關(guān)的工作
})
}
你在考慮一個(gè)不同的場(chǎng)景,例如作一些預(yù)初始化?沒(méi)問(wèn)題,只需要將它們加到 withDBContext 就可以了。這對(duì)于測(cè)試也同樣有效。
這個(gè)方法有個(gè)缺陷,它增加了縮進(jìn)并且更難閱讀。再次提示,永遠(yuǎn)尋找最簡(jiǎn)單的解決方案。
0.0.10 10. 為訪問(wèn) map 增加 setter,getters
如果你重度使用 map 讀寫(xiě)數(shù)據(jù),那么就為其添加 getter 和 setter 吧。通過(guò) getter 和 setter 你可以將邏輯封分別裝到函數(shù)里。這里最常見(jiàn)的錯(cuò)誤就是并發(fā)訪問(wèn)。如果你在某個(gè) goroutein 里有這樣的代碼:
m["foo"] = bar
還有這個(gè):
delete(m, "foo")
會(huì)發(fā)生什么?你們中的大多數(shù)應(yīng)當(dāng)已經(jīng)非常熟悉這樣的競(jìng)態(tài)了。簡(jiǎn)單來(lái)說(shuō)這個(gè)競(jìng)態(tài)是由于 map 默認(rèn)并非線程安全。不過(guò)你可以用互斥量來(lái)保護(hù)它們:
mu.Lock() m["foo"] = "bar" mu.Unlock()
以及:
mu.Lock() delete(m, "foo") mu.Unlock()
假設(shè)你在其他地方也使用這個(gè) map。你必須把互斥量放得到處都是!然而通過(guò) getter 和 setter 函數(shù)就可以很容易的避免這個(gè)問(wèn)題:
func Put(key, value string) {
mu.Lock()
m[key] = value
mu.Unlock()
}
func Delete(key string) {
mu.Lock()
delete(m, key)
mu.Unlock()
}
使用接口可以對(duì)這一過(guò)程做進(jìn)一步的改進(jìn)。你可以將實(shí)現(xiàn)完全隱藏起來(lái)。只使用一個(gè)簡(jiǎn)單的、設(shè)計(jì)良好的接口,然后讓包的用戶(hù)使用它們:
type Storage interface {
Delete(key string)
Get(key string) string
Put(key, value string)
}
這只是個(gè)例子,不過(guò)你應(yīng)該能體會(huì)到。對(duì)于底層的實(shí)現(xiàn)使用什么都沒(méi)關(guān)系。不光是使用接口本身很簡(jiǎn)單,而且還解決了暴露內(nèi)部數(shù)據(jù)結(jié)構(gòu)帶來(lái)的大量的問(wèn)題。
但是得承認(rèn),有時(shí)只是為了同時(shí)對(duì)若干個(gè)變量加鎖就使用接口會(huì)有些過(guò)分。理解你的程序,并且在你需要的時(shí)候使用這些改進(jìn)。
0.0.11 總結(jié)
抽象永遠(yuǎn)都不是容易的事情。有時(shí),最簡(jiǎn)單的就是你已經(jīng)實(shí)現(xiàn)的方法。要知道,不要讓你的代碼看起來(lái)太聰明。Go 天生就是個(gè)簡(jiǎn)單的語(yǔ)言,在大多數(shù)情況下只會(huì)有一種方法來(lái)作某事。簡(jiǎn)單是力量的源泉,也是為什么在人的層面它表現(xiàn)的如此有彈性。
如果必要的話,使用這些基數(shù)。例如將 []Server 轉(zhuǎn)化為 Servers 是另一種抽象,僅在你有一個(gè)合理的理由的情況下這么做。不過(guò)有一些技術(shù),如 iota 從 1 開(kāi)始計(jì)數(shù)總是有用的。再次提醒,永遠(yuǎn)保持簡(jiǎn)單。
特別感謝 Cihangir Savas、Andrew Gerrand、Ben Johnson 和 Damian Gryski 提供的極具價(jià)值的反饋和建議。
如果你有任何疑問(wèn)請(qǐng)及時(shí)反饋, 請(qǐng)隨時(shí)在 Twitter上與我分享:[@fatih]
本文中的所有譯文僅用于學(xué)習(xí)和交流目的,轉(zhuǎn)載請(qǐng)務(wù)必注明文章譯者、出處、和本文鏈接
我們的翻譯工作遵照 [CC 協(xié)議](https: //learnku.com/docs/guide/cc4.0/6589),如果我們的工作有侵犯到您的權(quán)益,請(qǐng)及時(shí)聯(lián)系我們。
