简单列举一些 GoLang 中容易犯的错误。
数组
数组是值传递
如果需要在函数内修改,建议使用切片而非数组。
package main
import "fmt"
func main() {
x := [3]int{1, 2, 3}
y := []int{1, 2, 3}
func(a [3]int, b []int) {
a[0] = 7
fmt.Printf("inner %v\n", a)
b[0] = 7
fmt.Printf("inner %v\n", b)
}(x, y)
fmt.Printf("outer %v\n", x)
fmt.Printf("outer %v\n", y)
}
上述的变量中,y
作为切片,在函数内的修改会影响到函数外的变量,因为它修改的是指针。
异常处理
恢复的使用方式
recover()
函数必须在 defer
的函数中调用,不允许通过函数再次嵌套,不能直接调用,否则会失效。
package main
import "fmt"
func main() {
x := [3]int{1, 2, 3}
y := []int{1, 2, 3}
func(a [3]int, b []int) {
a[0] = 7
fmt.Printf("inner %v\n", a)
b[0] = 7
fmt.Printf("inner %v\n", b)
}(x, y)
fmt.Printf("outer %v\n", x)
fmt.Printf("outer %v\n", y)
}
调度
协程泄露
与很多高级语言类似,GoLang 自带了内存回收的特性,一般内存不会泄露,但可能会出现在 GC 的时候出现卡顿。
但是,对于协程来说就可能会出现泄露,同时该协程引用的对象也无法释放。
package main
import (
"context"
"fmt"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
ch := make(chan int)
go func() {
for i := 0; ; i++ {
// ch <- i
select {
case <-ctx.Done():
return
case ch <- i:
}
}
}()
for v := range ch {
fmt.Println(v)
if v == 5 {
cancel()
break
}
}
}
上述会起一个后台的协程向管道中添加数据,如果工作协程退出,那么后台协程就可能会泄露。其中 context 包是最常用的,可以有效的处理协程之间的嵌套调用。
独占CPU
协程采用的是协作式强占调度,本身不会主动放弃 CPU ,如果在某个协程中死循环,那么就可能会导致其它的协程无法调度。
package main
import (
"fmt"
"runtime"
)
func main() {
runtime.GOMAXPROCS(1)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}()
//for {} // this will hang forever.
for {
runtime.Gosched()
}
// OR 'select' for quit signal.
}
有两种规避的方式,在死循环中允许协程重新调度,或者使用阻塞的方式等待退出。