这是一篇讨论而不是结论,希望大家参与思考并说出自己的想法
我们在 Go 中处理 error 一般是这样的:
package main
func execute() error {
return nil
}
func main() {
err := execute()
if err != nil {
panic(err)
}
}
大多数场景我们知道发生了错误即可,所以这么处理没啥问题。如果需要根据不同的错误做不同的操作,一般有以下几种做法:
package main
import (
"io"
"os"
"strings"
)
func execute() error {
return nil
}
func main() {
err := execute()
if err != nil {
// 1. 根据错误信息判断
if strings.Contains(err.Error(), "timeout") {
// ...
}
// 2. 根据类型判断,断言
if _, ok := err.(*os.PathError); ok {
// ...
}
// 3. 根据类型判断,实例
if err == io.EOF {
// ...
}
panic(err)
}
}
很明显这三种可以达到目的,但都有自身的问题:
第一种,根据错误信息判断就会依赖错误信息,如果错误信息改了,这个逻辑就走不通了,而错误信息修改并不奇怪,所以这种方式是最不推荐的。
第二种和第三种,根据类型判断是大多数代码里用的比较多的,但这种方式需要知道调用的方法里面究竟会返回什么错误,换句话说,我们要知道我们感兴趣或者要去处理的错误逻辑对应的是哪个错误类型,要做到这点,就需要方法返回的错误都是公开的,并且要么以注释要么以文档的形式去告诉调用方会返回哪些错误(比如 Java 是方法声明上写明)。
这几种方式的问题在于,我们都要去窥探方法内部实现或者 error 的实现了,这显然是不够优雅的!有没有更优雅一点的处理方式呢?
有!参考 os 包中的几个方法:
package main
import (
"os"
)
func execute() error {
return nil
}
func main() {
err := execute()
if err != nil {
// 通过公开的错误判断
if os.IsPermission(err) {
// ...
}
panic(err)
}
}
这种方式优雅在哪?屏蔽了错误类型的细节,这本来也不应该是我们关心的,我们关心的是错误情况而不是类型(细品,你细品),这种方式就可以很好的把错误情况公开给我们,而内部具体是怎么判断的我们不需要知道。那这种方式就没有弊端了么?还是有的,但相比前几种要好很多,这也是我目前找到的比较优雅的处理方式了。
小栗子:
数据库找不到数据应该返回 error 还是 nil 还是别的标识?如果是返回 error,需不需要做错误的屏蔽?
比如我们写了个 dao
层,目前是访问 mysql
,返回的错误是 sql
的错误,后面切换为 mongo
,返回的错误就变了,所以这边最好是用这种方法做错误的屏蔽。
最后,我提出两个问题:
- 最后一种方式(指 os.IsPermission 这种方式)到底有啥弊端?(穿山甲到底说了啥?)
- 还有没有更优雅的处理方式?(这个是本文最核心的点,希望收到大家踊跃的回答)
闲聊
我们知道 error 在 Go 中其实是个接口,非常简单的设计,也很抽象,抽象到一些和错误相关的东西都没有了,必须依赖接口实现去完成,比如堆栈信息和错误的详细信息。这时候可以通过 github.com/pkg/errors
包来包装处理,结合 Go1.13 新加的 Is
和 As
方法可以做到比较好的错误处理,这里不细展开说。
package main
import (
"fmt"
"github.com/pkg/errors"
)
func doSomething() error {
err := fmt.Errorf("错误!%w", fmt.Errorf("未知错误"))
//return err
return errors.Wrap(err, "包装之后的错误")
}
func main() {
err := doSomething()
fmt.Printf("%+v\n", errors.Wrap(nil, "nil") == nil)
fmt.Printf("%+v\n", errors.Unwrap(err))
}