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