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的泄漏。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM