中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

Go 語言的 10 個實用技巧

2018-07-20    來源:編程學(xué)習(xí)網(wǎng)

容器云強(qiáng)勢上線!快速搭建集群,上萬Linux鏡像隨意使用

這里是我過去幾年中編寫的大量 Go 代碼的經(jīng)驗總結(jié)而來的自己的最佳實踐。我相信它們具有彈性的。這里的彈性是指:

某個應(yīng)用需要適配一個靈活的環(huán)境。

你不希望每過 3 到 4 個月就不得不將它們?nèi)恐貥?gòu)一遍。添加新的特性應(yīng)當(dāng)很容易。許多人參與開發(fā)該應(yīng)用,它應(yīng)當(dāng)可以被理解,且維護(hù)簡單。許多人使用該應(yīng)用,bug 應(yīng)該容易被發(fā)現(xiàn)并且可以快速的修復(fù)。我用了很長的時間學(xué)到了這些事情。其中的一些很微小,但對于許多事情都會有影響。

Go 語言的 10 個實用技巧0

所有這些都僅僅是建議,具體情況具體對待,并且如果有幫助的話務(wù)必告訴我。隨時留言:)

1. 使用單一的 GOPATH

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

許多人不同意這一觀點(diǎn),接下來我會做一些澄清。像 etcd 或camlistore 這樣的大項目使用了像 godep 這 樣的工具,將所有依賴保存到某個目錄中。也就是說,這些項目自身有一個單一的 GOPATH。它們只能在這個目錄里找到對應(yīng)的版本。除非你的項目很大并且極為重要,否則不要為每個項目使用不同的 GOPATH。如果你認(rèn)為項目需要一個自己的 GOPATH 目錄,那么就創(chuàng)建它,否則不要嘗試使用多個 GOPATH。它只會拖慢你的進(jìn)度。

2. 將 for-select 封裝到函數(shù)中

如果在某個條件下,你需要從 for-select 中退出,就需要使用標(biāo)簽。例如:

func main() { L: for { select { case <-time.After(time.Second): fmt.Println("hello") default: break L } } fmt.Println("ending") }

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

如果需要退出循環(huán),我會將 for-select 封裝到函數(shù)中:

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. 在初始化結(jié)構(gòu)體時使用帶有標(biāo)簽的語法

這是一個無標(biāo)簽語法的例子:

type T struct { Foo string
    Bar int } func main() { t := T{"example", 123} // 無標(biāo)簽語法 fmt.Printf("t %+v\n", t) }

那么如果你添加一個新的字段到T結(jié)構(gòu)體,代碼會編譯失。

type T struct { Foo string
    Bar int Qux string } func main() { t := T{"example", 123} // 無法編譯 fmt.Printf("t %+v\n", t) }

如果使用了標(biāo)簽語法,Go 的兼容性規(guī)則(http://golang.org/doc/go1compat)會處理代碼。例如在向net包的類型添加叫做Zone的字段,參見:http://golang.org/doc/go1.1#library;氐轿覀兊睦樱褂脴(biāo)簽語法:

type T struct { Foo string
    Bar int Qux string } func main() { t := T{Foo: "example", Qux: 123} fmt.Printf("t %+v\n", t) }

這個編譯起來沒問題,而且彈性也好。不論你如何添加其他字段到T結(jié)構(gòu)體。你的代碼總是能編譯,并且在以后的 Go 的版本也可以保證這一點(diǎn)。只要在代碼集中執(zhí)行g(shù)o vet,就可以發(fā)現(xiàn)所有的無標(biāo)簽的語法。

4. 將結(jié)構(gòu)體的初始化拆分到多行

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

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

而是:

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

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

5. 為整數(shù)常量添加 String() 方法

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

type State int const ( Running State = iota
    Stopped
    Rebooting
    Terminated )

如果你創(chuàng)建了這個類型的一個變量,然后輸出,會得到一個整數(shù)(http://play.golang.org/p/V5VVFB05HB):

func main() { state := Running // print: "state 0" fmt.Println("state ", state) }

除非你回顧常量定義,否則這里的0看起來毫無意義。只需要為State類型添加String()方法就可以修復(fù)這個問題(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。顯然現(xiàn)在看起來可讀性好了很多。在你調(diào)試程序的時候,這會帶來更多的便利。同時還可以在實現(xiàn) MarshalJSON()、UnmarshalJSON() 這類方法的時候使用同樣的手段。

6. 讓 iota 從 a +1 開始增量

在前面的例子中同時也產(chǎn)生了一個我已經(jīng)遇到過許多次的 bug。假設(shè)你有一個新的結(jié)構(gòu)體,有一個State字段:

type T struct { Name  string
    Port int State State }

現(xiàn)在如果基于 T 創(chuàng)建一個新的變量,然后輸出,你會得到奇怪的結(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 %+v\n", t) }

看到 bug 了嗎?State字段沒有初始化,Go 默認(rèn)使用對應(yīng)類型的零值進(jìn)行填充。由于State是一個整數(shù),零值也就是0,但在我們的例子中它表示Running。

那么如何知道 State 被初始化了?還是它真得是在Running模式?沒有辦法區(qū)分它們,那么這就會產(chǎn)生未知的、不可預(yù)測的 bug。不過,修復(fù)這個很容易,只要讓 iota 從 +1 開始(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 %+v\n", t) }

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

const ( Unknown State = iota
    Running
    Stopped
    Rebooting
    Terminated )

7. 返回函數(shù)調(diào)用

我已經(jīng)看過很多代碼例如(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() }

更簡單也更容易閱讀(當(dāng)然,除非你要對某些內(nèi)部的值做一些記錄)。

8. 把 slice、map 等定義為自定義類型

將 slice 或 map 定義成自定義類型可以讓代碼維護(hù)起來更加容易。假設(shè)有一個Server類型和一個返回服務(wù)器列表的函數(shù):

type Server struct { Name string } func ListServers() []Server { return []Server{ {Name: "Server1"}, {Name: "Server2"}, {Name: "Foo1"}, {Name: "Foo2"}, } }

現(xiàn)在假設(shè)需要獲取某些特定名字的服務(wù)器。需要對 ListServers() 做一些改動,增加篩選條件:

// ListServers 返回服務(wù)器列表。只會返回包含 name 的服務(wù)器。空的 name 將會返回所有服務(wù)器。 func ListServers(name string) []Server { servers := []Server{ {Name: "Server1"}, {Name: "Server2"}, {Name: "Foo1"}, {Name: "Foo2"}, } // 返回所有服務(wù)器 if name == "" { return servers } // 返回過濾后的結(jié)果 filtered := make([]Server, 0) for _, server := range servers { if strings.Contains(server.Name, name) { filtered = append(filtered, server) } } return filtered }

現(xiàn)在可以用這個來篩選有字符串Foo的服務(wù)器:

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

顯然這個函數(shù)能夠正常工作。不過它的彈性并不好。如果你想對服務(wù)器集合引入其他邏輯的話會如何呢?例如檢查所有服務(wù)器的狀態(tài),為每個服務(wù)器創(chuàng)建一個數(shù)據(jù)庫記錄,用其他字段進(jìn)行篩選等等……

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

type Servers []Server // ListServers 返回服務(wù)器列表 func ListServers() Servers { return []Server{ {Name: "Server1"}, {Name: "Server2"}, {Name: "Foo1"}, {Name: "Foo2"}, } }

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

// Filter 返回包含 name 的服務(wù)器。空的 name 將會返回所有服務(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)在可以針對字符串Foo篩選服務(wù)器:

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

哈!看到你的代碼是多么的簡單了嗎?還想對服務(wù)器的狀態(tài)進(jìn)行檢查?或者為每個服務(wù)器添加一條數(shù)據(jù)庫記錄?沒問題,添加以下新方法即可:

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

9. withContext 封裝函數(shù)

有時對于函數(shù)會有一些重復(fù)勞動,例如鎖/解鎖,初始化一個新的局部上下文,準(zhǔn)備初始化變量等等……這里有一個例子:

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)的工作 }

如果你想要修改某個內(nèi)容,你需要對所有的都進(jìn)行修改。如果它是一個常見的任務(wù),那么最好創(chuàng)建一個叫做withContext的函數(shù)。這個函數(shù)的輸入?yún)?shù)是另一個函數(shù),并用調(diào)用者提供的上下文來調(diào)用它:

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

只需要將之前的函數(shù)用這個進(jìn)行封裝:

func foo() { withLockContext(func() { // foo 相關(guān)工作 }) } func bar() { withLockContext(func() { // bar 相關(guān)工作 }) } func qux() { withLockContext(func() { // qux 相關(guān)工作 }) }

不要光想著加鎖的情形。對此來說最好的用例是數(shù)據(jù)庫鏈接,F(xiàn)在對 withContext 函數(shù)作一些小小的改動:

func withDBContext(fn func(db DB) error) error { // 從連接池獲取一個數(shù)據(jù)庫連接 dbConn := NewDB() return fn(dbConn) }

如你所見,它獲取一個連接,然后傳遞給提供的參數(shù),并且在調(diào)用函數(shù)的時候返回錯誤。你需要做的只是:

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)工作 }) }

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

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

10. 為訪問 map 增加 setter,getters

如果你重度使用 map 讀寫數(shù)據(jù),那么就為其添加 getter 和 setter 吧。通過 getter 和 setter 你可以將邏輯封分別裝到函數(shù)里。這里最常見的錯誤就是并發(fā)訪問。如果你在某個 goroutein 里有這樣的代碼:

m["foo"] = bar

還有這個:

delete(m, "foo")

會發(fā)生什么?你們中的大多數(shù)應(yīng)當(dāng)已經(jīng)非常熟悉這樣的競態(tài)了。簡單來說這個競態(tài)是由于 map 默認(rèn)并非線程安全。不過你可以用互斥量來保護(hù)它們:

mu.Lock() m["foo"] = "bar" mu.Unlock()

以及:

mu.Lock() delete(m, "foo") mu.Unlock()

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

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

使用接口可以對這一過程做進(jìn)一步的改進(jìn)。你可以將實現(xiàn)完全隱藏起來。只使用一個簡單的、設(shè)計良好的接口,然后讓包的用戶使用它們:

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

這只是個例子,不過你應(yīng)該能體會到。對于底層的實現(xiàn)使用什么都沒關(guān)系。不光是使用接口本身很簡單,而且還解決了暴露內(nèi)部數(shù)據(jù)結(jié)構(gòu)帶來的大量的問題。

但是得承認(rèn),有時只是為了同時對若干個變量加鎖就使用接口會有些過分。理解你的程序,并且在你需要的時候使用這些改進(jìn)。

總結(jié)

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

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

特別感謝 Cihangir Savas、Andrew Gerrand、Ben Johnson 和 Damian Gryski 提供的極具價值的反饋和建議。


標(biāo)簽: 安全 代碼 服務(wù)器 數(shù)據(jù)庫

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點(diǎn)!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請與原作者聯(lián)系。

上一篇:系統(tǒng)剖析 Android 中的內(nèi)存泄漏

下一篇:Java調(diào)優(yōu)經(jīng)驗談