复习语法 在 2018 年 8 月,误处官方正式公布了 Go 2 Draft Designs[2],理提其中包含泛型和错误处理机制改进的批判初步草案: Go 2 Draft Designs 下面是关键的 Go2 错误处理新语法。 第一个要解决的误处问题就是大量 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 Values)、错误检查(Error Inspection)的问题,其引申出错误值打印(Error Printing)的问题,云南idc服务商也可以认为是错误格式化的不便利。 官方针对此提出了提出了 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 该提案增加了错误链的 Wrapping Error 概念,并同时增加 errors.Is 和 errors.As 的方法,与前面说到的 Go1.13 的改进一致,不再赘述。 Go1.13 没有实现 %+v 输出调用堆栈的需求(没有调用栈,排查问题会很苦恼),因为此举会破坏 Go1 兼容性和产生一些性能问题,大概会在 Go2 加入。 目标较为模糊 在 Go2 新错误处理的提案和草案中,@Liam Breck 认为其没有去讨论根本的需求。仅有一个简短的目标部分,如下四点: 更也没有提到未来可能的高防服务器扩展性,只是纯粹的满足当下的诉求。这类是模糊的,在激发新的设计思路上有局限性。 无法统一错误处理 在真实的应用中,一个函数使用两种及以上的重复错误处理方式是非常常见的。 如下代码: // 方式 1 { debug.PrintStack(); log.Fatal(err) } // 方式 2 { log.Println(err) } // 方式 3 { if err == io.EOF { break } } // 方式 4 新提案中,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 清晰。 如下代码: 另外嵌套代码会助长不可读的结构: 现在回顾一下,该语言禁止。 是不是显得有些讽刺呢? 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!: [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