几年前写过一篇文章,讨论了一种处理错误的方式。 近几年,在不需要压榨性能,或者不需要做覆盖测试的场景里,我都会用这种更简短的方式。
所以,对于 try 提案,我是支持的,因为用 try 可以替代 panic / recover 实现隐式的 return,在性能方面有优势。 try 提案寿终正寝了,我是觉得可惜的。很多反对 try 提案的人,都不是出于技术性的原因,很多都是非理性的。 当然,技术性的缺陷也是有的,但我认为还没有严重到,连试验都不做就要放弃的地步。
说回前一篇文章所讨论的方式,最近其实也有改进,单独做了一个包:https://github.com/reusee/e
和以前的实现比较,有下述几个不同点:
首先,panic 抛出的,是经过包装的 thrownError 对象,而不是原先的 error 对象。 这样做的好处是,可以准确判断 recover 返回的,是不是由错误处理的 panic 抛出的对象。 以前的做法就比较粗放,很可能会将一些需要 panic 的对象,转化成了 error 返回值。
第二个不同是,error 类型可以有不同的实现。 check 和 catch 函数不再是全局函数,而是由一个构造函数返回,而这个构造函数的参数,是一个包装错误的函数。 构造函数的签名为
func New(
makeErr MakeErr,
) (
check func(err error, args ...interface{}),
catch func(errp *error, args ...interface{}),
) {
type MakeErr = func(error, ...interface{}) error
这种设计,比起之前使用固定的 Error 类型,要更灵活。 因为不同的项目,使用的 Error 可能不同,应该允许项目自己实现 makeErr。
当然这个包也自带了一个 makeErr 的实现,支持错误链、文件和行号等等, 也实现了标准库新加入的 Unwrap 方法,能使用 Is、As 等,足够好用了。
check 和 catch 的实现和前一篇文章的区别不大。
try 提案里的 CopyFile 函数,用这个包处理错误的话,是这样的
func CopyFile(src, dst string) (err error) {
defer catch(&err, "copy %s to %s", src, dst)
r, err := os.Open(src)
ce(err, "open %s", src)
defer r.Close()
w, err := os.Create(dst)
ce(err, "create %s", dst)
defer func() {
w.Close()
if err != nil {
os.Remove(dst)
}
}()
_, err := io.Copy(w, r)
ce(err)
ce(w.Close())
return nil
}
如果 try 提案实现了的话,ce(err, “open %s”, src) 就可以写成 try(wrap(err, “open %s”))。 wrap 在 err 不为 nil 时才包装。
据说泛型实现之后,错误处理的方式也会重新考虑。 结合泛型的话,估计还是走那种 monadic 的路数吧。 但做成 rust 那样,我是不喜欢的。 我喜欢一个语句接一个语句地表达逻辑,而不是用链式调用。 本来这种方式,应该结合类似 do notation 的机制的,至少没有一堆行首的 .