這裡列舉的使用Go語言常遇到的問題都是符合Go語言語法的,可以正常編譯,但是可能出現運行結果錯誤,或者是有資源洩漏的風險。
A.1 可變參數是空接口類型
當參數的可變參數是空接口類型時,傳入空接口的切片時需要注意參數展開的問題:
<code>func main() { var a = []interface{}{1, 2, 3} fmt.Println(a) fmt.Println(a...)}/<code>
不管是否展開,編譯器都無法發現錯誤,但是輸出是不同的:
<code>[1 2 3]1 2 3/<code>
A.2 數組是值傳遞
在函數調用參數中,數組是值傳遞,無法通過修改數組類型的參數返回結果:
<code>func main() { x := [3]int{1, 2, 3} func(arr [3]int) { arr[0] = 7 fmt.Println(arr) }(x) fmt.Println(x)}/<code>
必要時需要使用切片。
A.3 map遍歷時順序不固定
map是一種散列表實現,每次遍歷的順序都可能不一樣:
<code>func main() { m := map[string]string{ "1": "1", "2": "2", "3": "3", } for k, v := range m { println(k, v) }}/<code>
A.4 返回值被屏蔽
在局部作用域中,命名的返回值被同名的局部變量屏蔽:
<code>func Foo() (err error) { if err := Bar(); err != nil { return } return}/<code>
A.5 recover()必須在defer函數中運行
recover()捕獲的是祖父級調用時的異常,直接調用是無效的:
<code>func main() { recover() panic(1)}/<code>
直接調用defer也是無效的:
<code>func main() { defer recover() panic(1)}/<code>
defer調用時多層嵌套依然無效:
<code>func main() { defer func() { func() { recover() }() }() panic(1)}/<code>
必須在defer函數中直接調用才有效:
<code>func main() { defer func() { recover() }() panic(1)}/<code>
A.6 main()函數提前退出
後臺Goroutine無法保證完成任務:
<code>func main() { go println("hello")}/<code>
A.7 通過Sleep()來回避併發中的問題
休眠並不能保證輸出完整的字符串:
<code>func main() { go println("hello") time.Sleep(time.Second)}/<code>
類似的還有通過插入調度語句:
<code>func main() { go println("hello") runtime.Gosched()}/<code>
A.8 獨佔CPU導致其他Goroutine餓死
Goroutine是協作式搶佔調度,Goroutine本身不會主動放棄CPU:
<code>func main() { runtime.GOMAXPROCS(1) go func() { for i := 0; i < 10; i++ { fmt.Println(i) } }() for {} // 佔用CPU}/<code>
解決的方法是在for循環中加入runtime.Gosched()調度函數:
<code>func main() { runtime.GOMAXPROCS(1) go func() { for i := 0; i < 10; i++ { fmt.Println(i) } }() for { runtime.Gosched() }}/<code>
或者是通過阻塞的方式避免CPU佔用:
<code>func main() { runtime.GOMAXPROCS(1) go func() { for i := 0; i < 10; i++ { fmt.Println(i) } os.Exit(0) }() select{}}/<code>
A.9 不同Goroutine之間不滿足順序一致性內存模型
因為在不同的Goroutine,main()函數中無法保證能打印出“hello, world”:
<code>var msg stringvar done boolfunc setup() { msg = "hello, world" done = true}func main() { go setup() for !done { } println(msg)}/<code>
解決的辦法是用顯式同步:
<code>var msg stringvar done = make(chan bool)func setup() { msg = "hello, world" done /<code>
msg的寫入是在通道發送之前,所以能保證打印“hello, world”。
A.10 閉包錯誤引用同一個變量
下面的代碼最終將輸出相同的值:
<code>func main() { for i := 0; i < 5; i++ { defer func() { println(i) }() }}/<code>
改進的方法是在每輪迭代中生成一個局部變量:
<code>func main() { for i := 0; i < 5; i++ { i := i defer func() { println(i) }() }}/<code>
或者是通過函數參數傳入:
<code>func main() { for i := 0; i < 5; i++ { defer func(i int) { println(i) }(i) }}/<code>
A.11 在循環內部執行defer語句
defer在函數退出時才能執行,在for執行defer會導致資源延遲釋放:
<code>func main() { for i := 0; i < 5; i++ { f, err := os.Open("/path/to/file") if err != nil { log.Fatal(err) } defer f.Close() }}/<code>
解決的方法可以在for中構造一個局部函數,在局部函數內部執行defer:
<code>func main() { for i := 0; i < 5; i++ { func() { f, err := os.Open("/path/to/file") if err != nil { log.Fatal(err) } defer f.Close() }() }}/<code>
A.12 切片會導致整個底層數組被鎖定
切片會導致整個底層數組被鎖定,底層數組無法釋放內存。如果底層數組較大會對內存產生很大的壓力:
<code>func main() { headerMap := make(map[string][]byte) for i := 0; i < 5; i++ { name := "/path/to/file" data, err := ioutil.ReadFile(name) if err != nil { log.Fatal(err) } headerMap[name] = data[:1] } // do some thing}/<code>
解決的方法是將結果克隆一份,這樣可以釋放底層的數組:
<code>func main() { headerMap := make(map[string][]byte) for i := 0; i < 5; i++ { name := "/path/to/file" data, err := ioutil.ReadFile(name) if err != nil { log.Fatal(err) } headerMap[name] = append([]byte{}, data[:1]...) } // do some thing}/<code>
A.13 空指針和空接口不等價
例如,返回了一個錯誤指針,但是並不是空的error接口:
<code>func returnsError() error { var p *MyError = nil if bad() { p = ErrBad } return p // Will always return a non-nil error.}/<code>
A.14 內存地址會變化
Go語言中對象的地址可能發生變化,因此指針不能從其他非指針類型的值生成:
<code>func main() { var x int = 42 var p uintptr = uintptr(unsafe.Pointer(&x)) runtime.GC() var px *int = (*int)(unsafe.Pointer(p)) println(*px)}/<code>
當內存發生變化的時候,相關的指針會同步更新,但是非指針類型的uintptr不會做同步更新。
同理CGO中也不能保存Go對象地址。
A.15 Goroutine洩漏
Go語言帶有內存自動回收的特性,因此內存一般不會洩漏。但是Goroutine確實存在洩漏的情況,同時洩漏的Goroutine引用的內存同樣無法被回收。
<code>func main() { ch := func() /<code>
上面的程序中後臺Goroutine向通道輸入自然數序列,main()函數中輸出序列。但是當break跳出for循環的時候,後臺Goroutine就處於無法被回收的狀態了。
我們可以通過context包來避免這個問題:
<code>func main() { ctx, cancel := context.WithCancel(context.Background()) ch := func(ctx context.Context) /<code>
當main()函數在break跳出循環時,通過調用cancel()來通知後臺Goroutine退出,這樣就避免了Goroutine的洩漏。
本文摘自《Go語言高級編程》柴樹杉,曹春暉 著。
- Go語言進階實戰,CGO編程web編程書
- 雲計算雲存儲區塊鏈時代重要編程語言
- 滿足Gopher好奇心的Go語言進階讀物
本書聚焦於主流Go語言書中缺失的或刻意迴避的部分主題,主要面向希望深入瞭解Go語言,特別是對Go語言和其他語言的混合編程、Go彙編語言的工作機制、構造Web框架和分佈式開發等領域感興趣的學生、工程師和研究人員。閱讀本書需要讀者對Go語言有一定的認識和使用經驗。
本書關於CGO編程和Go彙編語言的講解在中國乃至全球Go語言出版物中是非常有特色的。
本書主要內容● Go語言演化歷史。● CGO編程技術。● Go彙編語言。● RPC和gRPC。● 構造Web框架的方法。● 分佈式系統。
閱讀更多 程序員書屋 的文章