原文鏈接:https://chai2010.cn/advanced-go-programming-book/appendix/appendix-a-trap.html
這里列舉的Go語言常見坑都是符合Go語言語法的,可以正常的編譯,但是可能是運(yùn)行結(jié)果錯誤,或者是有資源泄漏的風(fēng)險。
當(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ù)調(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是一種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捕獲的是祖父級調(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)
}
后臺Goroutine無法保證完成任務(wù)。
func main() {
go println("hello")
}
休眠并不能保證輸出完整的字符串:
func main() {
go println("hello")
time.Sleep(time.Second)
}
類似的還有通過插入調(diào)度語句:
func main() {
go println("hello")
runtime.Gosched()
}
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,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)
}
}
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ù)組被鎖定,底層數(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.
}
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對象地址。
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的泄漏。
更多建議: