Go 语言由于没有 try...catch
结构屡屡被诟病,Go 中的每一个错误都需要处理,而且错误经常是蹭蹭嵌套的。如下面的结构:
a, err := fn() if err != nil { return err } func fn() error { b, err := fn1() if err != nil { … return err } if _, err = fn2(); err != nil { … } }
Go Error 也是接口
在 Go 语言中,Go Error 也是一个接口:
type error interface { Error() string }
所以,实现、创建或抛出错误,实际上就是实现这个接口。最常见的三种方式是:
errors.New
fmt.Errof
implement error interface
var ErrRecordNotExist = errors.New("record not exist") func ErrFileNotExist(filename string) error { return fmt.Errorf("%s file not exist", filename) } type ErrorCallFailed struct { Funcname string } func (*ErrorCallFailed) Error() string { return fmt.Sprintf(“call %s failed”, funcname) } var ErrGetFailed error = &ErrorCallFailed{ Funcname: "getName", }
Go 错误只涉及以下两个逻辑:
- 抛出错误,这涉及到我们如何定义错误。在实现功能时,异常情况需要返回合理的错误
- 处理错误。调用函数时,要根据函数的返回实现不同的逻辑,考虑是否有错误,错误是否属于某种类型,是否忽略错误等。
func (d *YAMLToJSONDecoder) Decode(into interface{}) error { bytes, err := d.reader.Read() if err != nil && err != io.EOF { return err } if len(bytes) != 0 { err := yaml.Unmarshal(bytes, into) if err != nil { return YAMLSyntaxError{err} } } return err } type YAMLSyntaxError struct { err error } func (e YAMLSyntaxError) Error() string { return e.err.Error() }
Kubernetes decode.go 中的这篇文章不仅可以直接返回错误,还可以包装错误,要么返回 YAMLSyntaxError
,要么直接忽略 io.EOF
通常,有三种方法可以确定错误类型:
- 直接通过
==
, 例如:if err == ErrRecordNotExist {}
- 类型推断,
if _, ok := err.(*ErrRecordNotExist); ok {}
errors.Is
和errors.As
方法. 从 Go 1.13 开始添加。if errors.Is(err, ErrRecordNotExist)
涉及错误换行,解决了定位嵌套错误的麻烦。
遵循的规则
理解了 Go 错误的基本概念之后,是时候讨论可以遵循的规则以进行更好的实践了。让我们从定义开始,然后到错误处理。
定义错误
- 使用 fmt.Errorf 而不是
errors.New
fmt.Errorf
提供拼接参数功能,并对错误进行包装。虽然我们在处理简单错误时发现这两种方法没有区别,但始终将 fmt.Errorf
设置为您的偏好可以保持代码统一。
封装相同的错误
封装同样的错误,比如上面提到的 ErrorCallFailed
,是一种常见的代码优化,结合 errors.Is
或者errors.As
可以解包层,更好的判断错误的真正原因。 至于 errors.Is
和 errors.As
的区别,前者既需要类型匹配又需要消息匹配,而后者只需要类型匹配。
func fn(n string) error { if _, err := get(n); err != nil { return ErrorCallFailed("get n") } } func abc() error { _, err = fn("abc") if err != nil { return fmt.Errorf("handle abc failed, %w", err) } } func main() { _, err := abc() if errors.Is(err, ErrorCallFailed){ log.Fatal("failed to call %v", err) os.Exist(1) } }
使用 %w 而不是 %v
一个方法被多处调用时,为了得到完整的调用链,开发者会在返回错误的地方一层一层的包裹起来,通过fmt.Errorf
不断添加当前调用的唯一特征,可以是日志也可以是一个参数。在错误拼接中偶尔使用 %v 而不是 %w 会导致 Go 的错误包装功能在 Go1.13 和之后的版本中失效。正确换行后的错误类型如下