关于Go错误处理新提案的一个想法:?操作符这样用行不行

开发 前端
Ian Taylor在discussion中明确了该提案的目标是引入一种新语法,在不影响控制流清晰度的前提下,减少正常情况下检查错误所需的代码量。

0. 背景

Ian Taylor在关闭了旨在消除Go错误处理样板代码的issue[1]之后,又另起了一个“同名”的discussion[2]。错误处理真不愧是Go社区呼声最高的问题,几天之内又收到了近500条回复!不过到目前为止,依然没有形成统一和高赞的意见。

关于error handling的样板代码过多,其实我个人是可以接受的,即便不做出任何改变也是ok的,估计Go社区与我有相同看法的也不在少数。比如就有人引用了Rob Pike的权威观点[3],并认为Go应该按照Rob大神的思路,保持Go语法稳定:  

图片

  不过自然也会有另外一批人强烈希望错误处理的样板代码得到改进。

Ian Taylor在discussion中明确了该提案的目标是引入一种新语法,在不影响控制流清晰度的前提下,减少正常情况下检查错误所需的代码量。  

 Ian最初的Proposal由于隐式声明变量err以及可选代码块等问题而备受“批评”,并且似乎该proposal违反了他自己提出的目标。  

今天在discussion中看到一位名为Mukunda Johnson的gopher的评论[4],我觉得很有道理。其核心观点就是:尽量保持Go的传统语法形式。他还给出了期望中的语法示例:

// 当前错误处理样板代码过多的示例
f, err := open(file)
if err != nil {
   return err
}
defer f.Close()

if err = binwrite(f, signature); err != nil {
   return err
}
if err = binwrite(f, header); err != nil {
   return err
}
if err = binwrite(f, zeroSegment); err != nil {
   return err
}

for _, s := range segments {
   if err = binwrite(f, s); err != nil {
      return err
   }
}

if err = binwrite(f, footer); err != nil {
   return err
}

vs. 

// 使用新语法改进后的代码
f, err := open(file)?
defer f.Close()

binwrite(f, signature)?
binwrite(f, header)?
binwrite(f, zeroSegment)?

for _, s := range segments {
   binwrite(f, s)?
}

binwrite(f, footer)?

这给了我很大启发:我们可以引入?语法,但是如果结合原先err变量的声明形式岂不是更好!比如:

f, err := open(file)?

岂不是要比下面两种形式更好!

f := open(file)?

或

f := open(file)? err { }

通过仅引入一个问号(?)操作符,并避免引入过多的新语法形式,却能解决60%的错误处理样板问题。根据jba对Go开源代码中错误处理的抽样统计[5],超过60%的错误处理都是直接返回err,而没有对err进行任何修饰。此外,显式声明err可以最大程度地避免隐式声明带来的问题,同时提升代码的可读性。

因此,基于尽量使用已有Go代码风格、最大程度避免隐式声明,并仅解决最常见的错误处理样板代码的原则,下面我基于Ian提案的错误处理改进语法,谈点自己关于新?操作符使用的想法,大家看看是否可行。

1. 对于最常见的未经修饰的错误处理代码

err := SomeFunction2()
if err != nil {
 return err
}

或是

if err := SomeFunction2(); err != nil {
    return err
}

我们使用下面的新语法做等价替代:

err := SomeFunction2() ?

如果声明的错误变量名为err,也可省略赋值操作符左侧代码,从而简化为:

SomeFunction2() ? // 这里略带隐式

2. 如果函数返回值有多个,甚至有多个错误变量的情况

比如下面代码:

a, b, err0, err1, err2 := SomeFunction3()
if err2 != nil {
    return err2
}

我们可以将其改写为:

a, b, err0, err1, err2 := SomeFunction3()?

其语义是如果err2不为nil,返回err2,但前提要保证赋值语句的左侧的最后一个变量err2必须是实现error接口的类型的变量。

如果是像下面这样在err2 != nil时有多个返回值,又该如何处理呢?

a, b, err0, err1, err2 := SomeFunction3()
if err2 != nil {
    return a, b, err2
}

对于这种情况,我认为可以不在新方案的考虑范围之内,现在怎么写,请继续这么写。如果非要解决,请继续看后面支持可选代码块的情况。

实现以上两种情况,就能解决60%以上的错误样板代码问题了!

3. 对于对返回的error值进行修饰的情况

对于像下面两种对返回的error变量进行修饰的情况:

r, err := SomeFunction()
if err != nil {
    return fmt.Errorf("something failed: %v", err)
}

if err := SomeFunction2(); err != nil {
    return fmt.Errorf("something else failed: %v", err)
}

我的第一想法是保持现状 ,不在新方案考虑范围之内。

不过如果非要在新方案中解决,那就需要引入可选代码块(optional block)了!比如:

r, err := SomeFunction() ? {
    return fmt.Errorf("something failed: %v", err)
}

err := SomeFunction2() ? {
    return fmt.Errorf("something else failed: %v", err)
}

和Ian的原proposal中语法不同,这里我们依然显式声明了err,当然你也可以不用err这个名字,由于是显式声明,你用任何名字均可,比如:

r, e := SomeFunction() ? {
    return fmt.Errorf("something failed: %v", e)
}

myErr := SomeFunction2() ? {
    return fmt.Errorf("something else failed: %v", myErr)
}

这将避免隐式声明带来的诸多问题!

基于可选代码块,我们也可以处理一下前面提到的返回多个值的情况。下面代码

a, b, err0, err1, err2 := SomeFunction3()
if err2 != nil {
    return a, b, err2
}

可以改写为:

a, b, err0, err1, err2 := SomeFunction3() ? {
    return a, b, err2
}

这里加入可选代码块后,我建议开发人员负责显式调用return,而不是由?操作符来自动return,也就是说完全将控制权交给你。如果你没有在可选代码块中调用return,那么代码在执行完可选代码块中的代码后,还会继续向下执行。可选代码块相当于一个error handler,而不带可选代码块的情况,默认的error handler其实就是一个return err,伪代码类似这样:

err := SomeFunction2() ?

<=>

err := SomeFunction2() ? {
 return err
}

这样解释后,你是不是觉得在语义层面,不带可选代码块与带有可选代码块的情况就统一和一致了呢!

本质上来说,?+可选代码块仅是让你少敲了个if以及err != nil。

4. 综合示例

Mukunda Johnson给出的示例其实已经可以很好地展示?操作符+显式声明err方案带来的消除样板代码的效果,这里再回顾一下(这里没用到可选代码块,因此代码显得格外清晰):

f, err := open(file)?
defer f.Close()

binwrite(f, signature)?
binwrite(f, header)?
binwrite(f, zeroSegment)?

for _, s := range segments {
   binwrite(f, s)?
}

binwrite(f, footer)?

此外,在原discussion中,另外一个gopher提出的示例,我们也可以用上面的想法改写一下:

// 最常见的情况
SomeFunc() ?

// 多个返回值,最后一个为error变量
a, err1 := SomeFunction2() ?

// 返回前对err进行修饰
err := SomeFunc() ? {
  return fmt.Errorf("oh no: %w", err)
}

// 显式声明避免变量遮蔽
err := SomeFunc() ?  {
  otherErr := OtherFunc() ?  {
    err = errors.Wrap(err, otherErr) // 在可选代码块中没有显式调用return,代码还会继续向后执行
  }
  return fmt.Errorf("oh no: %w", err)
}

5. 小结

再来简单总结一下上面想法中的语法形式的优势:

  • 与传统Go语法形式几乎一致,尽量避免引入过多新语法形式,在不使用可选代码块的时候,只是多了一个问号(?)。
  • 显式声明err变量,最大程度避免隐式声明带来的问题。
  • 专注解决最常见的错误处理样板情景,其他场景保持当前写法即可。
  • 即便引入可选代码块,本质上与不用可选代码块的语法在语义层面也是统一和一致的。

这一语法方案保留了原Ian提案中的优势,并能消除一些缺点,如变量遮蔽和隐式声明等。不过,仍然有些原proposal中的劣势问题无法完全消除,但这些问题显然不是主要关注点。

需要注意的是,以上想法目前仅停留在形式讨论层面,技术层面是否可行尚不确定。

大家认为我的想法可行吗?希望大家能提出更具建设性的意见^_^。

参考资料

[1] 旨在消除Go错误处理样板代码的issue: https://github.com/golang/go/issues/71203

[2] discussion: https://github.com/golang/go/discussions/71460

[3] Rob Pike的权威观点: https://go.dev/talks/2015/simplicity-is-complicated.slide#9

[4] Mukunda Johnson的gopher的评论: https://github.com/golang/go/discussions/71460#discussioncomment-12084482

[5] jba对Go开源代码中错误处理的抽样统计: https://github.com/golang/go/issues/71203#issuecomment-2585915103

责任编辑:武晓燕 来源: TonyBai
相关推荐

2024-06-05 08:47:20

Go语言方式

2022-07-13 08:53:28

函数Go语言

2022-09-05 08:55:15

Go2提案语法

2022-10-24 08:55:13

Go工具链开发者

2021-09-27 15:33:48

Go 开发技术

2021-09-27 10:04:03

Go程序处理

2024-11-19 09:10:19

迭代器Go语言

2014-11-17 10:05:12

Go语言

2021-04-29 09:02:44

语言Go 处理

2021-09-13 07:53:31

Go错误处理

2020-12-17 06:25:05

Gopanic 模式

2023-03-10 08:48:29

2020-08-20 10:16:56

Golang错误处理数据

2022-12-12 08:53:53

Go版本方式

2024-02-28 08:54:57

switchGo错误

2013-03-18 10:31:22

JS异常

2020-03-03 15:42:33

Python字典合并代码

2021-04-15 08:55:51

Go struc代码

2022-05-26 08:53:47

Go函数代码

2024-03-14 09:35:54

Go 错误select代码
点赞
收藏

51CTO技术栈公众号