必讀乾貨:使用Go語言常遇到的問題

這裡列舉的使用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語言常遇到的問題

  • Go語言進階實戰,CGO編程web編程書
  • 雲計算雲存儲區塊鏈時代重要編程語言
  • 滿足Gopher好奇心的Go語言進階讀物

本書聚焦於主流Go語言書中缺失的或刻意迴避的部分主題,主要面向希望深入瞭解Go語言,特別是對Go語言和其他語言的混合編程、Go彙編語言的工作機制、構造Web框架和分佈式開發等領域感興趣的學生、工程師和研究人員。閱讀本書需要讀者對Go語言有一定的認識和使用經驗。

本書關於CGO編程和Go彙編語言的講解在中國乃至全球Go語言出版物中是非常有特色的。

本書主要內容● Go語言演化歷史。● CGO編程技術。● Go彙編語言。● RPC和gRPC。● 構造Web框架的方法。● 分佈式系統。


分享到:


相關文章: