Go 語言常見坑

Go 語言常見坑

這裏列舉的 Go 語言常見坑都是符合 Go 語言語法的,可以正常的編譯,但是可能是運行結果錯誤,或者是有資源泄漏的風險。

可變參數是空接口類型

當參數的可變參數是空接口類型時,傳入空接口的切片時需要注意參數展開的問題。

func main() {
    var a = []interface{}{1, 2, 3}

    fmt.Println(a)
    fmt.Println(a...)
}

不管是否展開,編譯器都無法發現錯誤,但是輸出是不同的:

[1 2 3]
1 2 3

數組是值傳遞

在函數調用參數中,數組是值傳遞,無法通過修改數組類型的參數返回結果。

func main() {
    x := [3]int{1, 2, 3}

    func(arr [3]int) {
        arr[0] = 7
        fmt.Println(arr)
    }(x)

    fmt.Println(x)
}

必要時需要使用切片。

map 遍歷是順序不固定

map 是一種 hash 表實現,每次遍歷的順序都可能不一樣。

func main() {
    m := map[string]string{
        "1": "1",
        "2": "2",
        "3": "3",
    }

    for k, v := range m {
        println(k, v)
    }
}

返回值被屏蔽

在局部作用域中,命名的返回值內同名的局部變量屏蔽:

func Foo() (err error) {
    if err := Bar(); err != nil {
        return
    }
    return
}

recover 必須在 defer 函數中運行

recover 捕獲的是祖父級調用時的異常,直接調用時無效:

func main() {
    recover()
    panic(1)
}

直接 defer 調用也是無效:

func main() {
    defer recover()
    panic(1)
}

defer 調用時多層嵌套依然無效:

func main() {
    defer func() {
        func() { recover() }()
    }()
    panic(1)
}

必須在 defer 函數中直接調用纔有效:

func main() {
    defer func() {
        recover()
    }()
    panic(1)
}

main 函數提前退出

後臺 Goroutine 無法保證完成任務。

func main() {
    go println("hello")
}

通過 Sleep 來回避併發中的問題

休眠並不能保證輸出完整的字符串:

func main() {
    go println("hello")
    time.Sleep(time.Second)
}

類似的還有通過插入調度語句:

func main() {
    go println("hello")
    runtime.Gosched()
}

獨佔 CPU 導致其它 Goroutine 餓死

Goroutine 是協作式搶佔調度,Goroutine 本身不會主動放棄 CPU:

func main() {
    runtime.GOMAXPROCS(1)

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(i)
        }
    }()

    for {} // 佔用CPU
}

解決的方法是在 for 循環加入 runtime.Gosched() 調度函數:

func main() {
    runtime.GOMAXPROCS(1)

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(i)
        }
    }()

    for {
        runtime.Gosched()
    }
}

或者是通過阻塞的方式避免 CPU 佔用:

func main() {
    runtime.GOMAXPROCS(1)

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(i)
        }
        os.Exit(0)
    }()

    select{}
}

不同 Goroutine 之間不滿足順序一致性內存模型

因爲在不同的 Goroutine,main 函數中無法保證能打印出hello, world:

var msg string
var done bool

func setup() {
    msg = "hello, world"
    done = true
}

func main() {
    go setup()
    for !done {
    }
    println(msg)
}

解決的辦法是用顯式同步:

var msg string
var done = make(chan bool)

func setup() {
    msg = "hello, world"
    done <- true
}

func main() {
    go setup()
    <-done
    println(msg)
}

msg 的寫入是在 channel 發送之前,所以能保證打印hello, world

閉包錯誤引用同一個變量

func main() {
    for i := 0; i < 5; i++ {
        defer func() {
            println(i)
        }()
    }
}

改進的方法是在每輪迭代中生成一個局部變量:

func main() {
    for i := 0; i < 5; i++ {
        i := i
        defer func() {
            println(i)
        }()
    }
}

或者是通過函數參數傳入:

func main() {
    for i := 0; i < 5; i++ {
        defer func(i int) {
            println(i)
        }(i)
    }
}

在循環內部執行 defer 語句

defer 在函數退出時才能執行,在 for 執行 defer 會導致資源延遲釋放:

func main() {
    for i := 0; i < 5; i++ {
        f, err := os.Open("/path/to/file")
        if err != nil {
            log.Fatal(err)
        }
        defer f.Close()
    }
}

解決的方法可以在 for 中構造一個局部函數,在局部函數內部執行 defer:

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()
        }()
    }
}

切片會導致整個底層數組被鎖定

切片會導致整個底層數組被鎖定,底層數組無法釋放內存。如果底層數組較大會對內存產生很大的壓力。

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
}

解決的方法是將結果克隆一份,這樣可以釋放底層的數組:

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
}

空指針和空接口不等價

比如返回了一個錯誤指針,但是並不是空的 error 接口:

func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = ErrBad
    }
    return p // Will always return a non-nil error.
}

內存地址會變化

Go 語言中對象的地址可能發生變化,因此指針不能從其它非指針類型的值生成:

func main() {
    var x int = 42
    var p uintptr = uintptr(unsafe.Pointer(&x))

    runtime.GC()
    var px *int = (*int)(unsafe.Pointer(p))
    println(*px)
}

當內存發送變化的時候,相關的指針會同步更新,但是非指針類型的 uintptr 不會做同步更新。

同理 CGO 中也不能保存 Go 對象地址。

Goroutine 泄露

Go 語言是帶內存自動回收的特性,因此內存一般不會泄漏。但是 Goroutine 確存在泄漏的情況,同時泄漏的 Goroutine 引用的內存同樣無法被回收。

func main() {
    ch := func() <-chan int {
        ch := make(chan int)
        go func() {
            for i := 0; ; i++ {
                ch <- i
            }
        } ()
        return ch
    }()

    for v := range ch {
        fmt.Println(v)
        if v == 5 {
            break
        }
    }
}

上面的程序中後臺 Goroutine 向管道輸入自然數序列,main 函數中輸出序列。但是當 break 跳出 for 循環的時候,後臺 Goroutine 就處於無法被回收的狀態了。

我們可以通過 context 包來避免這個問題:

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    ch := func(ctx context.Context) <-chan int {
        ch := make(chan int)
        go func() {
            for i := 0; ; i++ {
                select {
                case <- ctx.Done():
                    return
                case ch <- i:
                }
            }
        } ()
        return ch
    }(ctx)

    for v := range ch {
        fmt.Println(v)
        if v == 5 {
            cancel()
            break
        }
    }
}

當 main 函數在 break 跳出循環時,通過調用cancel()來通知後臺 Goroutine 退出,這樣就避免了 Goroutine 的泄漏。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/hZwIArhfFS7P-nIDC03QWA