对 Go2 错误处理提案的批判

大家好,我是煎鱼。

一年半前分享了《先睹为快,Go2 Error 的挣扎之路》的文章,内容涉及 Go1 错误处理的问题、Go1.13 的加强、Go2 的新错误处理提案的详解。有多少小伙伴还记得 Go2 的新错误提案的 “美好” 未来?

当时 Go2 的新提案也遭受到了不少批评,@Liam Breck 在《Golang, how dare you handle my checks![1]》中对此进行了批判,让我们一起来学习吧!

复习语法

在 2018 年 8 月,官方正式公布了 Go 2 Draft Designs[2],其中包含泛型和错误处理机制改进的初步草案:

944612a5345a27ed0f6c449949f9d8dc.jpeg
Go 2 Draft Designs

下面是关键的 Go2 错误处理新语法。

错误处理(Error Handling)

第一个要解决的问题就是大量 if err != nil 的问题,针对此提出了 Go2 error handling[3] 的草案设计。

简单例子:

if err != nil {
 return err
}

优化后的方案如下:

func CopyFile(src, dst string) error {
 handle err {
  return fmt.Errorf("copy %s %s: %v", src, dst, err)
 }

 r := check os.Open(src)
 defer r.Close()

 w := check os.Create(dst)
 handle err {
  w.Close()
  os.Remove(dst) // (only if a check fails)
 }

 check io.Copy(w, r)
 check w.Close()
 return nil
}

主函数:

func main() {
 handle err {
  log.Fatal(err)
 }

 hex := check ioutil.ReadAll(os.Stdin)
 data := check parseHexdump(string(hex))
 os.Stdout.Write(data)
}

该提案引入了两种新的语法形式,首先是 check 关键字,其可以选中一个表达式 check f(x, y, z)check err,其将会标识这是一个显式的错误检查。

其次引入了 handle 关键字,用于定义错误处理程序流转,逐级上抛,依此类推,直到处理程序执行 return 语句,才正式结束。

错误值打印(Error Printing)

第二个要解决的问题是错误值(Error Values)、错误检查(Error Inspection)的问题,其引申出错误值打印(Error Printing)的问题,也可以认为是错误格式化的不便利。

官方针对此提出了提出了 Error Values[4] 和 Error Printing[5] 的草案设计。

简单例子如下:

if err != nil {
 return fmt.Errorf("write users database: %v", err)
}

优化后的方案如下:

package errors

type Wrapper interface {
 Unwrap() error
}

func Is(err, target error) bool
func As(type E)(err error) (e E, ok bool)

该提案增加了错误链的 Wrapping Error 概念,并同时增加 errors.Iserrors.As 的方法,与前面说到的 Go1.13 的改进一致,不再赘述。

Go1.13 没有实现 %+v 输出调用堆栈的需求(没有调用栈,排查问题会很苦恼),因为此举会破坏 Go1 兼容性和产生一些性能问题,大概会在 Go2 加入。

对提案批判

目标较为模糊

在 Go2 新错误处理的提案和草案中,@Liam Breck 认为其没有去讨论根本的需求。仅有一个简短的目标部分,如下四点:

  • 占用空间小的错误检查。

  • 对开发人员友好的错误处理。

  • 显式检查和处理错误。

  • 保证现有 Go1 代码的兼容性。

更也没有提到未来可能的扩展性,只是纯粹的满足当下的诉求。这类是模糊的,在激发新的设计思路上有局限性。

无法统一错误处理

在真实的应用中,一个函数使用两种及以上的重复错误处理方式是非常常见的。

如下代码:

// 方式 1
{ debug.PrintStack(); log.Fatal(err) }

// 方式 2
{ log.Println(err) }

// 方式 3
{ if err == io.EOF { break } }

// 方式 4
{ conn.Write([]byte("oops: " + err.Error())) } // network server

新提案中,check 和 handle 函数并不提供多种处理错误的途径。这是一个明显的遗漏,也没法统一错误处理机制。

如此的话,check 和 handle 就完全是只加了一种新的方式,让原本的错误处理机制更加的繁杂。

混用 err != nil 和 check

handle 函数是后进先出的模式,只能从一个函数中跳出。也就是说它不能很友好的处理可恢复的错误内容。

但实际上,许多方法返回错误是很常见的。因此我们需要同时使用 err!= nil 和 check,显得非常的繁琐。

如下代码:

handle err { ... }
v, err := f()
if err != nil {
   if isBad(err) {
      check err
   }
   // recovery code
}

又是 if err != nil,又是 handle,又是 check 函数。

嵌套 check 更复杂

通过 check 函数,对返回错误的函数调用进行嵌套调用,将会模糊了操作的顺序。

虽然在大多数情况下,错误发生时的调用顺序应该是清楚的,但在 check 函数下会显得不如 if err != nil 清晰。

如下代码:

check step4(check step1(), check step3(check step2())

另外嵌套代码会助长不可读的结构:

f1(v1, check f2(check f3(check f4(v4), v3), check f5(v5))

现在回顾一下,该语言禁止。

f(t ? a : b) 和 f(a++)

是不是显得有些讽刺呢?

if err != nil 太好用

Go1 的错误处理程序太友好了,也就是:

if err != nil {
    ...
}

其挫败了 "提高开发人员编写错误处理程序的可能性" 的目标,它使得在没有上下文信息的情况下返回错误是很容易的。会降低对 handle、check 函数的使用频率,变成一个可有可无的东西。

注:个人感觉,这一点既像黑又像粉...原作者反串黑?当然,确实 if err != nil 很好上手。

复杂的错误链

对于下面的例子,看看它的感觉...

如下代码:

func f() error {
   handle err { return ... }           // finally this
   if ... {
      handle err { ... }               // not that
      for ... {
         handle err { ... }            // nor that
         ...
      }
   }
   handle err { ... }                  // secondly this
   ...
   if ... {
      handle err { ... }               // not that
      ...
   } else {
      handle err { ... }               // firstly this
      check thisFails()                // trigger
   }
}

显然,这段代码是 “难以捉摸” 的,我们必须用眼睛解析整个函数,理解整个错误处理的流程和顺序。

将会加大我们对程序的认知负担。

总结

通过对 Go2 错误处理的设计草案的复习,我们了解到了 check 和 handle 函数的用法和思路。再针对新的语法,又对可能发生的新问题进行了 “批判”。

总的来说,新的语法,在弊端上会增加既有的代码复杂度和可读性,可以引发各种奇怪的嵌套,还会与 if err != nil 产生重复,变成了一种新的处理方式(多了一种)。

是否会还不如 if err != nil 那么的纯粹?

推荐阅读

参考资料

[1]

Golang, how dare you handle my checks!: https://mnmnotmail.medium.com/golang-how-dare-you-handle-my-checks-d5485f991289

[2]

Go 2 Draft Designs: https://go.googlesource.com/proposal/+/master/design/go2draft.md

[3]

Go2 error handling: https://github.com/golang/proposal/blob/master/design/go2draft-error-handling-overview.md

[4]

Error Values: https://github.com/golang/proposal/blob/master/design/go2draft-error-values-overview.md

[5]

Error Printing: https://github.com/golang/proposal/blob/master/design/go2draft-error-printing.md

关注和加煎鱼微信,

获取一手业内消息和知识,拉你进交流群👇

7f40cb0e5d41a36c91d2fb236664332e.jpeg

dce43ac32a01f3aef09da4dd38119136.png

你好,我是煎鱼,出版过 Go 畅销书《Go 语言编程之旅》,再到获得 GOP(Go 领域最有观点专家)荣誉,点击蓝字查看我的出书之路

日常分享高质量文章,输出 Go 面试、工作经验、架构设计,加微信拉读者交流群,和大家交流!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值