先总结
- 成本不能太高:我不想给我用不到的功能用性能买单|我不想这个功能卖给我的用的性能太高
- 错误不能被忽略:你不能辜负前人设计的那么多错误类型,直接 try 大段一把梭哈
- 语法上要精炼:!(Golang, 你知道我想说什么.龙图.jpg)[]
- 具有丰富的特性:溯源、多态等
- ...
看看大家是怎么做的
C
#define ERROR_PERMISSION 13
- 成本低,只是一个常量,表示发生了某个错误
- 错误可以被忽略,可以不处理函数调用返回的 error
- 语法不太精炼?需要对 error 进行判断,以及可能会过度出现的
if(err ≠ 0) return err;
这种模板 - 基本不具备结构化特性
Java
throw RuntimeException("坏了!"); try { ... } catch( .. ) { ... } finally { ... }
- 成本不低,Exception 会做一些额外的工作(backtrace等),开发者需要为其买单
- 错误不可忽略能力取决于开发者的编程习惯,喜欢 try 一大段用一个 catch 一把梭哈那就有点忽略错误的意思,但默认情况下,没有定义 throws 的方法,无法直接调用会 throw Exception 的方法,有一定错误错误不可忽略能力
- 语法上精炼,虽然一堆 try catch 嵌套有些丑陋,在一些合理的设计下,如 Reactive 编程风格中可以优化这种啰嗦的结构
- 具备强大的结构化特性,这是 Java 的特色
Go
type error interface { Error() string }
- 成本低,error 可以是任何类型,只需要具备
Error() string
方法都可以成为 error 类型 - 错误可以忽略,可以写出
data, _ := doSomething()
这种代码忽略doSomething()
返回中第二个 error 数据 - 语法上不精炼:
if err ≠ nil { return err }
的结构到处都是,很丑陋 - 具备一些结构化特性,error 可以是任何类型,可以自己实现一套溯源或者使用官方给的 errors 包
Rust
enum Result<T, E> { Ok(T), Err(E), }
- 成本低,具体实现是一种 enum 类型,不会主动捕捉栈记录(由 backtrace 这个module管理并捕获),error可以是任何类型
- 错误不可忽略,直接 unwrap 获得 Result::Ok 的结果可能会 panic,必须处理才能得到结果,
#[must_use]
宏可以限制 linter 必须使用 Result 类型的结果 - 语法精炼,? 这种语法糖大幅减少传播时的模板代码
- 具有一些结构化特性,Error trait 的 source 方法可用于溯源,downcast + trait Any 可以模拟出多态/弱类型
再总结
在开篇提出的几个要求下,结合列举出的一些我比较熟悉的语言(也差不多是一系列错误风格的代表语言),观察来下,只有 Rust 能尽量满足了这些要求。
我经常吐槽 Go 的错误设计不行,但好像从来没怎么仔细想过一个语言合理的错误设计应该是怎么样的(虽然之前有研究过业务 API 的错误处理,它们可能有些不一样)。
面试中被提到了这个问题,我只回答上了 语法上要精炼
这一个想法(<- 被 Go 折磨导致的),最后交谈后,才知道原来这些我习以为常的东西,也是一个错误处理该有的设计。