第一句话:我被骗了。之前写Go专栏的时候写过一篇文章:Go专栏|错误处理:延迟、恐慌和恢复。有朋友留言说:道理我都懂了,但是还是不会用,总是出现莫名其妙的问题。出问题就对了??,这小东西很破,一不留神就会出错。所以,面对这种情况,我们今天不会理智。直接拿自己珍藏多年的代码,靠多年踩坑写BUG的经验,我要站起来跨过这个坑。1.让我们用一个简单的例子来热身:packagemainimport("fmt")funcmain(){deferfunc(){fmt.Println("first")}()deferfunc(){fmt.Println("second")}()fmt.Println("done")}Output:donesecondfirst这个比较简单,defer语句的执行顺序按照调用defer语句的相反顺序执行。2.这段代码有什么问题?for_,filename:=rangefilenames{f,err:=os.Open(filename)iferr!=nil{returnerr}deferf.Close()}这段代码其实是非常危险的,很可能会用完文件描述符。因为defer语句要到函数的最后一刻才会执行,也就是说永远不会关闭文件。所以记住,一定不要在for循环中使用defer语句。那么如何优化呢?可以为循环体单独写一个函数,这样循环体每次循环都会调用关闭函数。如下:for_,filename:=rangefilenames{iferr:=doFile(filename);err!=nil{returnerr}}funcdoFile(filenamestring)error{f,err:=os.Open(filename)iferr!=nil{returnerr}deferf.Close()}第三,看看这三个函数的输出是什么?packagemainimport("fmt")funca()(rint){deferfunc(){r++}()return0}funcb()(rint){t:=5deferfunc(){t=t+5}()returnt}funcc()(rint){deferfunc(rint){r=r+5}(r)return1}funcmain(){fmt.Println("a=",a())fmt.Println("b=",b())fmt.Println("c=",c())}公布答案:a=1b=5c=1你答对了吗?说实话,刚开始看到这个结果的时候,我还挺纳闷的,完全不知道这是怎么回事。但是可以看出这三个函数有一个共同的特点,它们都有一个命名的返回值,并且在函数中都引用了这个返回值。有两种引用方式:闭包和函数参数。先看a()函数:闭包通过r++修改外部变量,返回值变为1。等价于:funcaa()(rint){r=0//返回前执行defer函数func(){r++}()return}再看b()函数:闭包中只修改了局部变量t,而外部变量t不受影响,所以还是返回了5。等价于:funcbb()(rint){t:=5//赋值r=t//return之前,执行defer函数//defer函数不修改返回值r,只修改变量tfunc(){t=t+5}()return}最后是c函数:传参是值拷贝,实参不受影响,所以还是返回1。等价于:funccc()(rint){//赋值r=1//这里修改的r是函数形参的值//值拷贝,不影响实参值func(rint){r=r+5}(r)return}然后,为了避免写出这种令人惊讶的代码,在定义函数时最好不要使用命名的返回值。或者,如果使用,请不要延迟引用它。看下面两个例子:funcd()int{r:=0deferfunc(){r++}()returnr}funce()int{r:=0deferfunc(iint){i++}(r)return0}d=0e=0返回值符合预期,无需再绞尽脑汁去猜测。4.如果defer表达式的函数在panic后面,则函数无法执行。funcmain(){panic("a")deferfunc(){fmt.Println("b")}()}的输出如下,没有打印b。panic:agoroutine1[running]:main.main()xxx.go:87+0x4ceexitstatus2如果defer先到可以执行。funcmain(){deferfunc(){fmt.Println("b")}()panic("a")}输出:bpanic:agoroutine1[running]:main.main()xxx.go:90+0x4e7exitstatus2五、看看下面代码的执行顺序:funcG(){deferfunc(){fmt.Println("c")}()F()fmt.Println("继续执行")}funcF(){deferfunc(){iferr:=recover();err!=nil{fmt.Println("捕获异常:",err)}fmt.Println("b")}()panic("a")}funcmain(){G()}顺序如下:调用G()函数;调用F()函数;F()遇到panic,立即终止,不执行panic之后的代码;执行F()中的defer函数,遇到recover捕获错误,继续执行defer里面的代码,然后返回;执行G()函数的后续代码,最后执行G()中的defer函数。输出:捕获异常:ab继续执行c5.看下面代码的执行顺序:funcG(){deferfunc(){iferr:=recover();err!=nil{fmt.Println("捕获异常:",err)}fmt.Println("c")}()F()fmt.Println("继续执行")}funcF(){deferfunc(){fmt.Println("b")}()panic("a")}funcmain(){G()}序列如下:调用G()函数;调用F()函数;F()遇到panic,立即终止,不执行panic后的代码;执行F()的defer函数,因为没有recover,将panic抛给G();G()收到panic后不会执行后续代码,直接执行defer函数;F()抛出的异常a在defer中被捕获,然后继续执行,最后退出。输出:b捕获异常:ac六、看下面代码的执行顺序:funcG(){deferfunc(){fmt.Println("c")}()F()fmt.Println("继续执行")}funcF(){deferfunc(){fmt.Println("b")}()panic("a")}funcmain(){G()}顺序如下:调用G()函数;调用F()函数;如果在F()中遇到panic,会立即终止,不会执行panic之后的代码;F()中的defer函数会被执行,由于没有recover,会把panic丢到G()中;G()收到panic时不会执行后续代码直接执行defer函数;由于没有recover,直接抛出F()抛出的异常a,然后退出。输出:bcpanic:agoroutine1[running]:main.F()xxx.go:90+0x5bmain.G()xxx.go:82+0x48main.main()xxx.go:107+0x4a5exitstatus2七、看下面这段代码执行顺序:funcG(){deferfunc(){//recoveriferroutsidegoroutine:=recover();err!=nil{fmt.Println("Catchexception:",err)}fmt.Println("c")}()//创建goroutine调用F函数goF()time.Sleep(time.Second)}funcF(){deferfunc(){fmt.Println("b")}()//Goroutine内部抛出panicpanic("a")}funcmain(){G()}序列如下:调用G()函数;通过goroutine调用F()函数;F()中遇到panic,立即终止,不执行panic后的代码;executeF()中的defer函数将panic抛给G(),因为没有recover;因为goroutine内部没有recover,goroutine的外部函数,也就是G()函数无法被捕获,程序直接崩溃退出。输出:bpanic:agoroutine6[running]:main.F()xxx.go:96+0x5bcreatedbymain.Gxxx.go:87+0x57exitstatus28.最后说一下recover的返回值:deferfunc(){iferr:=recover();err!=nil{fmt.Println("Catchexception:",err.Error())}}()panic("a")recover返回接口{}类型而不是错误类型,所以如果你使用它像这样会报错:err.Errorundefined(typeinterface{}isinterfacewithnomethods)可以这样转换:deferfunc(){iferr:=recover();err!=nil{fmt.Println("Catchexception:",fmt.Errorf("%v",err).Error())}}()panic("a")或者直接打印结果:deferfunc(){iferr:=recover();err!=nil{fmt.Println("Catchexception:",err)}}()panic("a")输出:catchingexception:a以上就是本文的全部内容。其实写过其他语言的同学都知道,关闭文件句柄,释放锁等操作很容易忘记。Go语言通过defer很好的解决了这个问题,但是在使用的过程中还是要小心。本文总结了一些陷阱,希望能帮助大家少写bug。如果觉得有用,请点赞转发。文章中的脑图和源码已经上传到GitHub,需要的同学可以自行下载。源码地址:https://github.com/yongxinz/gopher/tree/main/sc本文转载自微信公众号「AlwaysBeta」,可通过以下二维码关注。转载本文请联系AlwaysBeta公众号。
