当前位置: 首页 > 科技观察

一个Go语言真实错误的教训

时间:2023-03-11 23:15:03 科技观察

在前几天写的代码中,我犯了几个典型的错误,造成了很大的麻烦。让我在这里重现它并从中学习。场景描述代码需要实现客户端和服务端之间的数据重传机制,通过write向服务端写入数据,读取服务端返回。一旦中途出现错误,尝试每隔1s重写和读取数据。当超过上下文时间时,重传失败。重传实现代码retry如下。funcretry(ctxcontext.Context)(datastring,errerror){LOOP:fori:=1;;i++{err=write()iferr==nil{res,err:=read()iferr==nil{data=string(res)returndata,err}}log.Printf("changedatafailed,err:%v,retrytimes:%d\n",err,i)select{case<-ctx.Done():log.Printf("retryfailed")breakLOOPcase<-time.After(1*time.Second):}}return"",err}读写服务端数据函数和调用重传代码mock如下。funcwrite()error{returnnil}funcread()([]byte,error){return[]byte("helloworld"),errors.New("thisisaerror")}funcmain(){ctx,_:=context.WithTimeout(context.Background(),5*time.Second)_,_=retry(ctx)time.Sleep(10*time.Second)}write返回err为nil,read返回非nil。在这种情况下,日志输出如下。2020/07/0509:30:57changedatafailed,错误:,重试次数:12020/07/0509:30:58changedatafailed,错误:,重试次数:22020/07/0509:30:59changedatafailed,错误:,retrytimes:32020/07/0509:31:00changedatafailed,err:,retrytimes:42020/07/0509:31:01changedatafailed,err:,retrytimes:52020/07/0509:31:02retryfailed原因从分析中可以看出,正如预期的那样:当出现错误时,重试写入和读取。即重传机制生效。但是,为什么日志中err为nil,而read方法的错误返回被吞掉了呢?经过排查,发现原因在于Go语法糖:=(短变量声明)的使用不当。err=write()iferr==nil{res,err:=read()iferr==nil{data=string(res)returndata,err}}log.Printf("changedatafailed,err:%v,重试次数:%d\n",err,i)重试时,err为声明的变量类型错误。由于read返回两个变量,小菜岛使用short变量声明res变量,接受read的第一个返回参数。但是,这会改变err的作用域:err变成一个局部变量。这是什么意思?即此时的err受到短变量声明的影响,成为一个新的声明对象,只能应用于内部区域。对于外部的log.Printf,它所指的err仍然是write方法生成的err对象。所以即使read方法返回的err不为空,log.Printf也会打印write方法的err结果,导致read的err内容被吞掉。因此,为了避免此类错误,相应的代码调整如下。varres[]byteres,err=read()iferr==nil{data=string(res)returndata,err}此时read返回err不为nil时,打印log如下。2020/07/0509:46:16changedatafailed,err:thisisaerror,retrytimes:12020/07/0509:46:17changedatafailed,err:thisisaerror,retrytimes:22020/07/0509:46:18changedatafailed,err:thisisaerror,20retry20times:307/0509:46:19changedatafailed,err:thisisaerror,retrytimes:42020/07/0509:46:20changedatafailed,err:thisisaerror,retrytimes:52020/07/0509:46:21retryfailed(:=)使用注意事项。:=表示声明+赋值。短变量声明不需要在左侧声明所有变量。如果在同一个词法块中声明了一些变量,那么对于这些??变量,短声明的行为就像一个赋值(同时改变了这些变量的范围)。2.异常判断规则在上面的场景代码中,是一个多级条件判断的情况,判断规则是err为nil。但这是一种不恰当的处理逻辑。合理的判断条件就是对异常情况进行判断,把正常的逻辑放在条件之外。那么,修改后的重试条件判断逻辑应该如下。funcretry(ctxcontext.Context)(datastring,errerror){LOOP:fori:=1;;i++{err=write()iferr!=nil{log.Printf("writedatafailed,err:%v,retrytimes:%d\n",err,i)select{case<-ctx.Done():log.Printf("retryfailed")breakLOOPcase<-time.After(1*time.Second):}continue}res,err:=read()iferr!=nil{log.Printf("readdatafailed,err:%v,retrytimes:%d\n",err,i)select{case<-ctx.Done():log.Printf("retryfailed")breakLOOPcase<-time.After(1*time.Second):}continue}data=string(res)returndata,err}return"",err}这样,正常处理流程的主要逻辑在最外层,而只有异常情况(err!=nil)才能进入异常处理逻辑。采用这种判断规则后,就不会再有多层条件嵌套,语法糖带来的问题也就不复存在了。