Go 语言一次真实的错误吞并的教训

开发 后端
在几天前写的代码中,犯了几个比较典型的错误,带来不小的麻烦。特在此复现一下,吸取教训。

在几天前写的代码中,犯了几个比较典型的错误,带来不小的麻烦。特在此复现一下,吸取教训。

[[332662]]

情景描述

代码中需要实现一个客户端与服务器的数据重传机制,通过write写数据给服务器,read读取服务器返回。一旦中途发生错误,每隔1s就尝试重新写读数据。当超过上下文时间,重传失败。重传实现代码retry如下。

  1. func retry(ctx context.Context) (data string, err error) { 
  2.  
  3. LOOP: 
  4.  for i:=1;;i++{ 
  5.   err = write() 
  6.   if err == nil{ 
  7.    res, err := read() 
  8.    if err == nil{ 
  9.     data = string(res) 
  10.     return data, err 
  11.    } 
  12.   } 
  13.  
  14.   log.Printf("change data failed, err: %v, retry times : %d\n", err, i) 
  15.  
  16.   select { 
  17.   case <-ctx.Done(): 
  18.    log.Printf("retry failed"
  19.    break LOOP 
  20.   case <-time.After(1 * time.Second): 
  21.   } 
  22.  } 
  23.  return "", err 

读写服务器数据函数和调用重传代码mock如下。

  1. func write() error { 
  2.  return nil 
  3.  
  4. func read() ([]byte, error) { 
  5.  return []byte("hello world"), errors.New("this is a error"
  6.  
  7. func main() { 
  8.  ctx,_ := context.WithTimeout(context.Background(),5*time.Second
  9.  _, _ = retry(ctx) 
  10.  time.Sleep(10*time.Second

write返回err为nil,read有非nil返回。这种情况下,日志输出如下。

  1. 2020/07/05 09:30:57 change data failed, err: <nil>, retry times : 1 
  2. 2020/07/05 09:30:58 change data failed, err: <nil>, retry times : 2 
  3. 2020/07/05 09:30:59 change data failed, err: <nil>, retry times : 3 
  4. 2020/07/05 09:31:00 change data failed, err: <nil>, retry times : 4 
  5. 2020/07/05 09:31:01 change data failed, err: <nil>, retry times : 5 
  6. 2020/07/05 09:31:02 retry failed 

原因分析

可以看到的是,如预想的一样:当发生错误时,就重新尝试write和read。即重传机制生效。但是,日志中为何err会为nil,read方法的错误返回被吞掉了?

经过排查,发现原因就在于——Go语法糖:=(短变量声明)的不当使用。

  1. err = write() 
  2.  if err == nil{ 
  3.   res, err := read() 
  4.   if err == nil{ 
  5.    data = string(res) 
  6.    return data, err 
  7.   } 
  8.  } 
  9.  
  10.  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内容被吞。

因此,为了避免此类错误发生,相应代码调整如下。

  1. var res []byte 
  2.  res, err = read() 
  3.  if err == nil{ 
  4.   data = string(res) 
  5.   return data, err 
  6.  } 

此时,当read返回err非nil时,日志打印如下。

  1. 2020/07/05 09:46:16 change data failed, err: this is a error, retry times : 1 
  2. 2020/07/05 09:46:17 change data failed, err: this is a error, retry times : 2 
  3. 2020/07/05 09:46:18 change data failed, err: this is a error, retry times : 3 
  4. 2020/07/05 09:46:19 change data failed, err: this is a error, retry times : 4 
  5. 2020/07/05 09:46:20 change data failed, err: this is a error, retry times : 5 
  6. 2020/07/05 09:46:21 retry failed 

总结

一、Go语法糖——短变量声明(:=)使用注意事项。

  • :=表示声明+赋值。
  •  短变量声明不需要声明所有在左边的变量。如果一些变量在同一个词法块中声明,那么对于这些变量,短声明的行为等同于赋值(同时更改了这些变量的作用域)。

二、异常判断规则

在上述场景代码中,是一个多层级条件判断的情形,其判断规则是err为nil。但这是一种不恰当的处理逻辑。合理的判断条件,是对异常情况作判断,而将正常逻辑置于条件之外。那么,修改后的retry条件判断逻辑应该如下所示。

  1. func retry(ctx context.Context) (data string, err error) { 
  2.  
  3. LOOP: 
  4.  for i:=1;;i++{ 
  5.   err = write() 
  6.   if err != nil{ 
  7.    log.Printf("write data failed, err: %v, retry times : %d\n", err, i) 
  8.    select { 
  9.    case <-ctx.Done(): 
  10.     log.Printf("retry failed"
  11.     break LOOP 
  12.    case <-time.After(1 * time.Second): 
  13.    } 
  14.    continue 
  15.   } 
  16.  
  17.   res, err := read() 
  18.   if err != nil{ 
  19.    log.Printf("read data failed, err: %v, retry times : %d\n", err, i) 
  20.    select { 
  21.    case <-ctx.Done(): 
  22.     log.Printf("retry failed"
  23.     break LOOP 
  24.    case <-time.After(1 * time.Second): 
  25.    } 
  26.    continue 
  27.   } 
  28.  
  29.   data = string(res) 
  30.   return data, err 
  31.  
  32.  } 
  33.  return "", err 

这样,正常的处理流程,其主逻辑均在最外层,只有异常情况(err!=nil)才进入异常处理逻辑。当采用这种判断规则之后,就不存在多层条件嵌套语句,由语法糖带来的问题,也不复存在。

责任编辑:未丽燕 来源: Go语言中文网
相关推荐

2021-12-20 10:15:16

zip密码命令网络安全

2015-04-28 15:31:09

2023-09-11 00:14:46

后端团队项目

2011-06-28 10:41:50

DBA

2021-12-28 06:55:09

事故订单号绩效

2022-12-09 08:52:51

Go匿名接口

2021-04-29 09:02:44

语言Go 处理

2014-11-17 10:05:12

Go语言

2015-08-17 14:50:19

亚马逊云平台应用迁移

2020-10-24 13:50:59

Python编程语言

2021-12-27 10:08:16

Python编程语言

2022-10-17 00:07:55

Go语言标准库

2024-01-04 07:49:00

Go语言方法

2022-03-02 09:01:07

CPU使用率优化

2011-04-07 11:20:21

SQLServer

2024-04-17 08:42:15

Go语言分布式锁

2012-08-28 09:21:59

Ajax查错经历Web

2021-11-01 17:29:02

Windows系统Fork

2020-01-14 11:17:33

Go并发Linux

2009-07-23 08:40:37

VMware迁移备份归档
点赞
收藏

51CTO技术栈公众号