在几天前写的代码中,犯了几个比较典型的错误,带来不小的麻烦。特在此复现一下,吸取教训。
情景描述
代码中需要实现一个客户端与服务器的数据重传机制,通过write写数据给服务器,read读取服务器返回。一旦中途发生错误,每隔1s就尝试重新写读数据。当超过上下文时间,重传失败。重传实现代码retry如下。
func retry(ctx context.Context) (data string, err error) {
LOOP:
for i:=1;;i++{
err = write()
if err == nil{
res, err := read()
if err == nil{
data = string(res)
return data, err
}
}
log.Printf("change data failed, err: %v, retry times : %d\n", err, i)
select {
case <-ctx.Done():
log.Printf("retry failed")
break LOOP
case <-time.After(1 * time.Second):
}
}
return "", err
}
读写服务器数据函数和调用重传代码mock如下。
func write() error {
return nil
}
func read() ([]byte, error) {
return []byte("hello world"), errors.New("this is a error")
}
func main() {
ctx,_ := context.WithTimeout(context.Background(),5*time.Second)
_, _ = retry(ctx)
time.Sleep(10*time.Second)
}
write返回err为nil,read有非nil返回。这种情况下,日志输出如下。
2020/07/05 09:30:57 change data failed, err: <nil>, retry times : 1
2020/07/05 09:30:58 change data failed, err: <nil>, retry times : 2
2020/07/05 09:30:59 change data failed, err: <nil>, retry times : 3
2020/07/05 09:31:00 change data failed, err: <nil>, retry times : 4
2020/07/05 09:31:01 change data failed, err: <nil>, retry times : 5
2020/07/05 09:31:02 retry failed
原因分析
可以看到的是,如预想的一样:当发生错误时,就重新尝试write和read。即重传机制生效。但是,日志中为何err会为nil,read方法的错误返回被吞掉了?
经过排查,发现原因就在于——Go语法糖:=(短变量声明)的不当使用。
err = write()
if err == nil{
res, err := read()
if err == nil{
data = string(res)
return data, err
}
}
log.Printf("change data failed, err: %v, retry times : %d\n", err, i)
在retry中,err是已被声明的变量类型error。由于read返回的是两个变量,故小菜刀在此利用短变量声明res变量,接受read的第一个返回参数。但是,此举会改变err的作用范围:err成为了一个局部变量。什么意思呢?即此时的err被短变量声明所作用,成为了新声明对象,它只能作用于内部区域了。对于外部log.Printf而言,其引用到的err还是write方法生成的err对象。因此,即使read方法返回的err不为空,log.Printf打印的还是write方法的err结果,导致read的err内容被吞。
因此,为了避免此类错误发生,相应代码调整如下。
var res []byte
res, err = read()
if err == nil{
data = string(res)
return data, err
}
此时,当read返回err非nil时,日志打印如下。
2020/07/05 09:46:16 change data failed, err: this is a error, retry times : 1
2020/07/05 09:46:17 change data failed, err: this is a error, retry times : 2
2020/07/05 09:46:18 change data failed, err: this is a error, retry times : 3
2020/07/05 09:46:19 change data failed, err: this is a error, retry times : 4
2020/07/05 09:46:20 change data failed, err: this is a error, retry times : 5
2020/07/05 09:46:21 retry failed
总结
一、Go语法糖——短变量声明(:=)使用注意事项。
1. :=表示声明+赋值。
2. 短变量声明不需要声明所有在左边的变量。如果一些变量在同一个词法块中声明,那么对于这些变量,短声明的行为等同于赋值(同时更改了这些变量的作用域)。
二、异常判断规则
在上述场景代码中,是一个多层级条件判断的情形,其判断规则是err为nil。但这是一种不恰当的处理逻辑。合理的判断条件,是对异常情况作判断,而将正常逻辑置于条件之外。那么,修改后的retry条件判断逻辑应该如下所示。
func retry(ctx context.Context) (data string, err error) {
LOOP:
for i:=1;;i++{
err = write()
if err != nil{
log.Printf("write data failed, err: %v, retry times : %d\n", err, i)
select {
case <-ctx.Done():
log.Printf("retry failed")
break LOOP
case <-time.After(1 * time.Second):
}
continue
}
res, err := read()
if err != nil{
log.Printf("read data failed, err: %v, retry times : %d\n", err, i)
select {
case <-ctx.Done():
log.Printf("retry failed")
break LOOP
case <-time.After(1 * time.Second):
}
continue
}
data = string(res)
return data, err
}
return "", err
}
这样,正常的处理流程,其主逻辑均在最外层,只有异常情况(err!=nil)才进入异常处理逻辑。当采用这种判断规则之后,就不存在多层条件嵌套语句,由语法糖带来的问题,也不复存在。
推荐阅读
喜欢本文的朋友,欢迎关注“Go语言中文网”:
Go语言中文网启用微信学习交流群,欢迎加微信:274768166,投稿亦欢迎
文章评论