Go: 错误是值 errors are values

2021-01-302026-07-103 出链2 引用

错误是值:可以携带信息、传递、检查、比较(控制流)

go
// 实现了 error 接口类型的值即错误
// go doc builtin.error
type error interface {
	Error() string
}

Go 的错误处理哲学:错误值须显式处理,把每个错误点变成一个明确的设计决策:这一层是否要处理、解释、包装,还是继续交给上层。

创建错误

  1. 错误类型(type FooError struct{}):实现error接口的类型
  2. 错误值(var ErrBar error):通过errors.New fmt.Errorf 返回的值;及1中定义的错误类型的值
go
// 以下两个都是go标准库内置的不可导出的错误类型的值
// go doc -u errors.errorString
errors.New("")
// go doc -u fmt.wrapError
// go doc -u fmt.wrapErrors
fmt.Errorf("%w", err) // NOTE: %w 表示wrap错误,可用errors.Unwrap获取这个错误
 
// 定义自己的错误类型:...

解错误包装

go
go doc errors.Unwrap

实现了Unwrap() error的err都是被封装的错误,可以用errors.Unwrap拿出底层错误; 前面提到fmt.Errorf(%w,err)就是在包装错误

比较错误

Go: 标准库 errors包提供了Is/As/AsType等方法比较判断错误

func Is(err, target error) bool 是判断错误值是否相等; func As(err error, target any) bool 是判断错误值是否是属于某个类型;

最佳实践

  1. 不要忽略错误!
  2. 返回错误的时候,携带(wrap)当前上下文!
  3. 边界:携带stacktrace,打印日志

局限:非静态类型,无法穷举

go的错误仅仅可以告知可能失败,而无法知道可能以哪些错误失败; go的错误在编译器语义不充分,无法借助编译器进行语义分析

函数签名只有error接口,编译器不知道函数会返回哪些具体错误; errors.Is/errors.As是运行时检查,不是编译期保证,漏掉某个错误分支编译器不会报错。 对比Rust/Zig的错误集编码在类型系统中,match/switch漏分支编译器直接报错。

但因为错误仅仅是实现了error接口的值,所以其他操作都很灵活:

  • 传播:return errreturn fmt.Errorf("...: %w", err),简单但啰嗦(if err != nil样板)
  • 组合:%w包装、errors.Join合并多个错误
  • 信息:任意struct实现error接口,想带什么就带什么
  • 恢复:if err != nil { 用默认值 }

Go用编译期穷举能力,换来了错误操作的极致灵活和简单。

  1. https://go.dev/blog/errors-are-values
  2. https://dev.to/lovestaco/error-handling-in-go-stop-panicking-start-wrapping-351d
  3. https://www.jetbrains.com/guide/go/tutorials/handle_errors_in_go
  4. https://robinsiep.com/blog/posts/go-errors/
  5. https://niketpatel.com/essays/why-go-cant-try

评论