Errors are values

0 阅读2分钟

"Values can be programmed, and since errors are values, errors can be programmed." —— Rob Pike

内置 error 状态

在 Go 语言之父 Rob Pike 的 errors-are-values(go.dev/blog/errors… 一文中,Rob Pike 为我们呈现了 Go 标准库中使用了避免 if err != nil 反复出现的一种代码设计思路。

比如有一些代码看起来像这样:

_, err = fd.Write(p0[a:b])
if err != nil {
    return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
    return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
    return err
}
// and so on

通过“内置 error 状态”这种设计思路后:

type errWriter struct {
 w io.Writer
 err error
}

func (e *errWriter) Write(p []byte) {
 if e.err != nil {
  return
 }
 _, e.err = e.w.Write(p)
}

func (e *errWriter) Err() error {
 return e.err
}

func do() {
 ew := &errWriter{
  w: ...
 }
 ew.Write(buf)
 ew.Write(buf)
 ...
 ...
 if ew.Err() != nil {
  return ew.Err()
 }
 return nil
}

这种方法显然是消除 if err != nil 代码片段重复出现的理想方法。可以看到,错误状态被封装在 errWriter 结构的内部了,errWriter 定义了一个 err 字段作为一个内部错误状态值,它与 errWriter 的实例绑定在了一起,并且在每次 Write 入口判断是否为 nil。一旦不为 nil,Write 其实什么都没做就返回了。

github.com/pkg/errors

使用 github.com/pkg/errors 库。

package main

import (
 "fmt"
 "github.com/pkg/errors"
)

type MyError struct {
 e string
}

func (e *MyError) Error() string {
 return e.e
}

func main() {

 err1 := errors.New("err1")
 err2 := errors.WithMessage(err1, "err2")
 err3 := errors.WithMessage(err2, "err3")
 fmt.Println(err3.Error()) // err3: err2: err1

 // 0. errors.Cause 方法来获取底层的错误。
 fmt.Println(errors.Cause(err3)) // err1

 // 1. 检视某个错误值是否是某特定的错误值。
 // errors.Is 方法会沿着该包装错误所在错误链(error chain),与链上所有被包装的错误(wrapped error)进行比较,直至找到一个匹配的错误
 fmt.Println(errors.Is(err3, err1)) // true

 // 2. 检视某个错误类型是否是某特定的自定义错误类型。
 // 有时需要通过自定义错误类型的构造错误值的方式来提供更多的错误上下文信息
 // errors.As 方法类似于通过类型断言判断一个 error 类型变量是否为特定的自定义错误类型
 err5 := &MyError{"my error type"}
 err6 := errors.WithMessage(err5, "err6")
 err7 := errors.WithMessage(err6, "err7")
 var e *MyError
 fmt.Println(errors.As(err7, &e)) // true

 // 3. 附加堆栈信息
 err8 := errors.Wrap(err7, "err8")
 fmt.Printf("%+v\n", err8)
 // my error type
 //err6
 //err7
 //err8
 //main.main
 // E:/go_project/ME/p02Demo/main.go:42
 //runtime.main
 // D:/Go/src/runtime/proc.go:204
 //runtime.goexit
 // D:/Go/src/runtime/asm_amd64.s:1374
}
使用 defer 包装 err 来消除冗余
func writeConfig(data interface{}) (err error) {
 defer func() {
  if err != nil {
   err = errors.WithMessage(err, "writing configuration")
  }
 }()
 b, err := json.Marshal(data)
 if err != nil {
  return err
 }
 if err = ioutil.WriteFile("config.json", b, 0644); err != nil {
  return err
 }
 return
}

如果某个函数调用了其他几个返回错误的函数,并且需要以相同的方式包装所有错误,则可以使用分配给命名返回值的延迟函数包装它们的 err。


References
go.dev/blog/errors…
dave.cheney.net/2016/04/27/…