Go 語言 附錄A:Go語言常見坑

2023-03-22 15:05 更新

原文鏈接:https://chai2010.cn/advanced-go-programming-book/appendix/appendix-a-trap.html


附錄A:Go語言常見坑

這里列舉的Go語言常見坑都是符合Go語言語法的,可以正常的編譯,但是可能是運(yùn)行結(jié)果錯誤,或者是有資源泄漏的風(fēng)險。

可變參數(shù)是空接口類型

當(dāng)參數(shù)的可變參數(shù)是空接口類型時,傳入空接口的切片時需要注意參數(shù)展開的問題。

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

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

不管是否展開,編譯器都無法發(fā)現(xiàn)錯誤,但是輸出是不同的:

[1 2 3]
1 2 3

數(shù)組是值傳遞

在函數(shù)調(diào)用參數(shù)中,數(shù)組是值傳遞,無法通過修改數(shù)組類型的參數(shù)返回結(jié)果。

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表實現(xiàn),每次遍歷的順序都可能不一樣。

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

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

返回值被屏蔽

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

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

recover必須在defer函數(shù)中運(yùn)行

recover捕獲的是祖父級調(diào)用時的異常,直接調(diào)用時無效:

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

直接defer調(diào)用也是無效:

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

defer調(diào)用時多層嵌套依然無效:

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

必須在defer函數(shù)中直接調(diào)用才有效:

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

main函數(shù)提前退出

后臺Goroutine無法保證完成任務(wù)。

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

通過Sleep來回避并發(fā)中的問題

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

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

類似的還有通過插入調(diào)度語句:

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

獨(dú)占CPU導(dǎo)致其它Goroutine餓死

Goroutine 是協(xié)作式搶占調(diào)度(Go1.14版本之前),Goroutine本身不會主動放棄CPU:

func main() {
    runtime.GOMAXPROCS(1)

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

    for {} // 占用CPU
}

解決的方法是在for循環(huán)加入runtime.Gosched()調(diào)度函數(shù):

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{}
}

Go1.14 版本引入基于系統(tǒng)信號的異步搶占調(diào)度,可以避免 Goroutine 餓死的情況。

不同Goroutine之間不滿足順序一致性內(nèi)存模型

因為在不同的Goroutine,main函數(shù)中無法保證能打印出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發(fā)送之前,所以能保證打印hello, world

閉包錯誤引用同一個變量

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

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

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

或者是通過函數(shù)參數(shù)傳入:

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

在循環(huán)內(nèi)部執(zhí)行defer語句

defer在函數(shù)退出時才能執(zhí)行,在for執(zhí)行defer會導(dǎo)致資源延遲釋放:

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中構(gòu)造一個局部函數(shù),在局部函數(shù)內(nèi)部執(zhí)行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()
        }()
    }
}

切片會導(dǎo)致整個底層數(shù)組被鎖定

切片會導(dǎo)致整個底層數(shù)組被鎖定,底層數(shù)組無法釋放內(nèi)存。如果底層數(shù)組較大會對內(nèi)存產(chǎn)生很大的壓力。

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
}

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

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.
}

內(nèi)存地址會變化

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

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

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

當(dāng)內(nèi)存發(fā)生變化的時候,相關(guān)的指針會同步更新,但是非指針類型的uintptr不會做同步更新。

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

Goroutine泄露

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

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向管道輸入自然數(shù)序列,main函數(shù)中輸出序列。但是當(dāng)break跳出for循環(huán)的時候,后臺Goroutine就處于無法被回收的狀態(tài)了。

我們可以通過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
        }
    }
}

當(dāng)main函數(shù)在break跳出循環(huán)時,通過調(diào)用cancel()來通知后臺Goroutine退出,這樣就避免了Goroutine的泄漏。



以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號