文章目录
本文将介绍 Golang 初学者容易菜的坑,希望广告 Gopher 避而远之。
1. Channel 与 Goroutine 泄露
当 channel 不恰当使用时,就可能导致 Goroutine 发生永久阻塞从而造成资源泄露。
先看一下 channel 不同状态下的读写与 close 操作的结果。
操作未关闭已关闭nil发数据阻塞或成功发送panic永久阻塞取数据阻塞或成功接收成功接收或零值永久阻塞关闭成功关闭panicpanic
1.1 发送不接收
对于一个已满的 channel(buffered channel 容量已满或是 unbuffered channel),继续向 其发送数据将会导致当前goroutine阻塞。为了避免这种情况需要使用其他机制通知发送者。
// 错误示例funcproduce()<-chanint{
ch :=make(chanint)gofunc(){deferclose(ch)for i :=0; i <10; i++{
ch <- i
}}()return ch
}funcmain(){
ch :=produce()for num :=range ch {if num ==2{// 不想接收了,直接退出吧break}
fmt.Println(num)}// 虽然此段代码能正常运行,但// produce产生goroutine将永远// 阻塞于 ch <- i上,造成资源泄露}// 修正funcproduce(doneCh chanstruct{})<-chanint{
ch :=make(chanint)gofunc(){deferclose(ch)
loop:for i :=0; i <10; i++{select{case ch <- i:case<-doneCh:break loop
}}}()return ch
}funcmain(){
doneCh :=make(chanstruct{})
ch :=produce(doneCh)for num :=range ch {if num ==2{// 不想接收了,先通知一下生产者close(doneCh)break}
fmt.Println(num)}}
1.2 接收不发送
与前述情况相反,若接收者一直在一个不会再产生数据的 channel 上等待,将导致其所在routine 阻塞而泄露。 在Go中从一个 closed channel 读取数据:
- 不会阻塞且获取对应类型的零值
- for-range将退出
- v, ok := <-ch中ok将为false 所以可以利用上述性质通知接收方结束数据读取。
// 错误示例funcproduce(doneCh chanstruct{})<-chanint{
ch :=make(chanint)gofunc(){select{case ch<-1:case<-doneCh:break}// 任务完成,直接退出}()return ch
}funcmain(){
doneCh :=make(chanstruct{})
ch :=produce(doneCh)for num :=range ch {
fmt.Println(num)}close(doneCh)// Output:// 1// fatal error: all goroutines are asleep - deadlock!}// 修正funcproduce(doneCh chanstruct{})<-chanint{
ch :=make(chanint)gofunc(){// 退出前先关闭channel防止有routine阻塞在上面deferclose(ch)select{case ch<-1:case<-doneCh:break}}()return ch
}funcmain(){
doneCh :=make(chanstruct{})
ch :=produce(doneCh)for num :=range ch {
fmt.Println(num)}close(doneCh)// Output:// 1}
1.3 nil channel
向 nil channel 发送和接收数据都将会导致阻塞。这种情况可能在我们定义 channel 时忘记初始化的时候发生。
funcmain(){deferfunc(){
time.Sleep(time.Second)
fmt.Println("num of routines: ", runtime.NumGoroutine())}()var ch chanintgofunc(){<-ch
// ch<-}()}
2. 跳出 for-switch 或 for-select
没有指定标签的 break 只会跳出 switch/select 语句, 若不能使用 return 语句跳出的话,可为 break 跳出标签指定的代码块。
注意 goto 虽然也能跳转到指定位置,但依旧会再次进入 for-switch,死循环。
// break 配合 label 跳出指定代码块funcmain(){
loop:for{switch{casetrue:
fmt.Println("breaking out...")// break // 死循环,一直打印 breaking out...break loop
}}
fmt.Println("out...")}
3.for 迭代变量
3.1 闭包中的for迭代变量
for 语句中的迭代变量在每次迭代中都会重用,即 for 中创建的闭包函数 接收到的参数始终是同一个变量,所以在 goroutine 开始执行时都会得到同一个迭代值:
// 错误示例funcmain(){
n :=2
wg := sync.WaitGroup{}
wg.Add(n)for i :=0; i < n; i++{gofunc(){defer wg.Done()
fmt.Print(i)}()}
wg.Wait()// Output:// 22}// 修正funcmain(){
n :=2
wg := sync.WaitGroup{}
wg.Add(n)for i :=0; i < n; i++{
num := i
gofunc(){defer wg.Done()
fmt.Print(num)}()/*
当然也可以这样
go func(num int) {
defer wg.Done()
fmt.Println(num)
}(i)
*/}
wg.Wait()// Output:// 01 或 10 }
3.2 for range 迭代变量
for range 循环中迭代变量的短声明只会在开始时执行一次,后面都是直接赋值,所以迭代变量的变量地址是不变的,避免将其赋值给指针。
// 错误示例
slice1 :=[]int32{1,2,3,4,5}
slice2 :=make([]*int32,len(slice1))for i, item :=range slice1 {
slice2[i]=&item
}for_, item :=range slice2 {
fmt.Printf("%v",*item)}// 55555// 修正funcInt32(v int32)*int32{return&v
}funcmain(){
slice1 :=[]int32{1,2,3,4,5}
slice2 :=make([]*int32,len(slice1))for i, item :=range slice1 {
slice2[i]=Int32(item)}for_, item :=range slice2 {
fmt.Printf("%v",*item)}// 12345}
4. 循环内的 defer
对 defer 延迟执行的函数,会在调用它的函数结束时执行,而不是在调用它的语句块结束时执行,注意区分开。
// 错误示例type Resource struct{/*内部有一些需要释放的内容 */}func(r Resource)Destroy(){/*...*/}funcgetResource() Resource {/*...*/}funcmain(){for i :=0; i <10000; i++{
res :=getResource()defer res.Destroy()// 会一直延迟至main结束才会释放// do something}}// 修正type Resource struct{/* 内部有一些需要释放的内容 */}func(r Resource)Destroy(){/*...*/}funcgetResource() Resource {/*...*/}funcmain(){for i :=0; i <10000; i++{func(){
res :=getResource()defer res.Destroy()// 下次循环前就会释放,当然你也可以在最后直接调用Destroy// do something}()}}
5.defer 函数的参数值
defer 只会延迟其后函数的执行,而不会延迟函数的参数的求值,若希望延迟其参数 求值,通常会加上一层匿名函数。
funcmain(){var i =1
times :=func(num int)int{return num *2}defer fmt.Println("resultA: ",times(i))deferfunc(){
fmt.Println("resultB: ",func()int{return i *2}())}()
i++// Output:// resultB: 4// resultA: 2}
6.nil interface 和 nil interface 值
Golang 中 interface 类型变量的实现中包含值与类型,只有两者都为 nil 时该变量才为nil。
// 错误示例type Foo interface{Bar()}type FooImpl struct{
num int}func(f *FooImpl)Bar(){ fmt.Println(f.num)}funcGenFoo(num int)(Foo,error){var f *FooImpl
if num !=0{
f =&FooImpl{num}}return f,nil}funcmain(){
f,_:=GenFoo(0)// this comparison is never trueif f ==nil{return}// Panic
f.Bar()}// 正确示例funcGenFoo(num int)(Foo,error){if num !=0{
f :=&FooImpl{num}return f,nil}returnnil, errors.New("num is zero")}
那么如何判断 interface{} 的值是否为 nil 呢?
funcIsNil(i interface{}){if i !=nil{if reflect.ValueOf(i).IsNil(){
fmt.Println("i is nil")return}
fmt.Println("i isn't nil")}
fmt.Println("i is nil")}
7.结构体指针访问属性前先判空
当结构体指针为nil时,直接访问结构体属性会报空指针
// 错误示例type Struct1 struct{
id int32}funcmain(){var a *Struct1
//panic: runtime error: invalid memory address or nil pointer dereference
a.id =1}// 修正type Struct1 struct{
id int32}funcmain(){var a *Struct1
if a !=nil{
a.id =1}}
8.读取有顺序需要的不能使用map结构
Go 里面的map存储是无序的,for循环读取与写入的顺序并不同,需要排序的功能不能使用map,而需要使用slice。
// map 读取情况
intMap :=make(map[int]int,10)for i :=0; i <10; i++{
intMap[i]= i
}for_, v :=range intMap {
fmt.Println(v)}//9//3//7//……//没有按照写入顺序输出,乱序的// slice 读取情况
intSlice :=make([]int,0,10)for i :=0; i <10; i++{
intSlice =append(intSlice, i)}for_, v :=range intSlice {
fmt.Println(v)}//0//1//2//...//读取是有序的
参考文献
Go 神坑 1 —— interface{} 与 nil 的比较 - CSDN
50 Shades of Go: Traps, Gotchas, and Common Mistakes
50 Shades of Go: Traps, Gotchas, and Common Mistakes中文翻译
如何防止 goroutine 泄露
版权归原作者 恋喵大鲤鱼 所有, 如有侵权,请联系我们删除。