精读了 Error handling 这一块的草案,第一感觉就是丑,丑爆,丑出天际...
用过 golang 的都知道,在 go 1.X 里面,错误处理靠海量的 if err != nil {} 来完成。但凡是可能抛出 error 的函数调用点,都要补这么一坨逻辑。都说一个好的编程语言像一把利器,比如很多人称 ruby 是程序员的瑞士军刀。那 golang 呢,用起来像上世纪八十年代那种打一枪还要撸一发的老式霰弹枪。如果这时候,你公司的那些连 Java 都写不利索的新人们还动不动给有大老板在的技术群里贴一贴各种来自微信公众号的低端安利文,把 go 1.X 这种错误处理的方式美名其曰`独立错误处理`,然后动不动去学 golang 社区流行的说话风格,谈吐中夹杂充满哲学气息的成语比如少即是多,最后终于有一天,轮到你来接盘 debug 他们写的 web 工程,进入一个 .go 文件的页面,ctrl + F 搜 err,诶呀吗一屏有26个结果,简直不要更上火。这时候你可能终于深刻领悟什么叫 少(语言特性少)即是多(苦逼的服务端程序员对说的就是你,要敲的键帽更多)。
好了以上是非恶意的吐槽,golang 的忠实信徒不要太认真。
---
那在 go 1.X 里面使用 if err != nil 的方式进行错误处理有什么问题呢,其实是有的。首先它失去在时间序上对异常处理的控制权。比如继续搬来民工语言 Java 举例子,你可以选择就地使用防御式编程的逻辑,当场处理掉异常;也可以把一大段连续的业务逻辑描述包在一个 try-catch 代码块里面,进行统一的异常处理,或者用 try-catch-finally 处理资源释放(还有 JDK 1.7 之后的 try-with-resource 或者 python 的 with 表达式,用起来更体贴)。这样本可以在不同情形下使用更合适的错误处理风格,而使用 golang 的话,基本只能被强制成,每一行调用接一个if err != nil {} 代码段这种方式,想要描述的业务逻辑被错误处理的逻辑各种割裂开来,很不利于自己行文组织的连续表达。
这种 if err != nil 还有个隐含的缺点,就是它太繁琐,变相鼓励程序员使用 '_' 来忽略绕开。这种案例还真发生过,去年前司的行情系统有一天在线上 panic,重启进程并不能解决。跟进到最后发现是当天深交所发了一个以前没有过的编码格式,而当初写行情系统的程序员在 decode 附近一处看起来绝不可能出问题的逻辑,自信满满的用了一个 '_' 最终导致异常发生。
接下来扯一下另一种偏学院派的风格,比如 Scala 的 Try[T]/Option[T]. 这种方式可以称为代数数据类型(Algebraic DataTypes) 更细分一点是 ADT 里的 sum types 也可以叫 Tagged Union. 它和 try-catch 风格的错误处理写起来有什么不同呢,简单说主要是无论这个 Try[T] 的实际类型是成功(Success[T]),还是失败(Failure[Throwable]) 它们都是一个类型。这样可以用高阶函数进行一系列行云流水的连续调用,这个过程你可以完全不 care 异常分支,只需要表述你的正常分支业务逻辑,我要从这里拿到 A 这个数据,decode 我服务能认识的 struct/class. 然后派发给某某组件调用,最后拿到结果。直到最后需要 consume 的一瞬间,接一个 pattern matching,把正常结果和异常分支一并处理掉,完美。比如这样子,(使用 RxJava 开发过 Android 的同志们应该也能体会到类似的感觉)
Rust 的 Result/Option 也同理,只不过 Rust 社区充斥着有爱,生怕语言使用者被这些陌生的概念吓到,哄小孩一样的循序渐进,而决口不提 Applicative/Monad 之类高端向的名词。
最后回到主题,如果想实现后者的风格,需要泛型和模式匹配的语法支持。缺任意一个都会让这种风格的表述不那么舒畅,想一下 TypeScript 里面使用 union type 最后发现没有 pattern matching 的蛋疼感,不过有一个总是比没有好的。golang 最终还是打算走一遍已经被证明还算靠谱的 try/catch 的老路(但是草案 example 里别扭的感觉基本起源于为了兼容 golang 1.X 多返回值返回 err 的方式,需要用 check 关键字标记返回 err 的函数调用点),但这样又打了自己当初特立独行标新立异的老脸,于是改名成 check/handle 试图护脸。
Over...