聊一聊 go 的 error

2,087 阅读3分钟

在 go 语言中使用 error 来表示异常「panic 不在此篇讨论」。至于为什么使用 error 而不是 excpetion。作者表示:语言的设计鼓励你去处理错误, 而不是像其他语言一样抛出异常「throws exception」, 然后再去捕获「try-catch」异常。 当然, 关于 go 在异常中的处理, 不同的人有不同的观点。有的认为: 使用 error 会导致代码中出现大量 「if err != nil」, 导致异常逻辑和正常业务逻辑混合在一起。有的认为:使用 error 可以使返回值的语义更加清晰。其实, 不管是 error 还是 exception 都是对异常处理的一种方式, 都有一定的优点和缺点, 这里就不进行过多讨论。

什么是 error

error 是一个接口类型, 下面是接口的定义。可以看出 error 其实是一个接口, 只要实现了 Error() 方法, 就可以看作是一个 error.

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
   Error() string
}

如何使用 error

在编写 go 函数时, 通常返回 result, error。error 用于返回异常信息, 表示函数发生了异常, 无法正常处理。如果你的函数返回的是 bool, 可以不用返回 error, res == false 可以表示 error.

构造 error

可以通过内置包 errors 和 fmt 构造一个 error, 表示函数发生的错误信息。在下面的例子中, 可以通过 errors.New 返回条错误信息, 如果需要进行模版表示, 则使用 fmt.Errof 函数。

func Sqrt(f float64) (float64, error) {
   if f < 0 {
      return 0, errors.New("math: square root of negative number")
   }
   // implementation
   return 0, nil
}

func Sqrt(f float64) (float64, error) {
   if f < 0 {
      return 0, fmt.Errorf("math: square root of negative number %g", f)
   }
   // implementation
   // return 0, nil
}

定义预期 error

系统开发中, 通常会定义预期的错误,方便我们进行错误判断和逻辑处理。

var InvalidParam = errors.New("math: square root of negative number")

func Sqrt(f float64) (float64, error) {
   if f < 0 {
      return 0, InvalidParam
   }
   // implementation
   return 0, nil
}

// 函数调用
_, err := Sqrt(-1.0)
if errors.Is(err, InvalidParam) {
   fmt.Println("get expected error")
}

error 扩展

简单的 err.Error() 会返回一个 string 信息, 但是简单的 string 未必满足复杂的业务场景。对于 http 请求, 业务上通常返回一个 code, 用于标示错误类型。可以选择自定义 bizError, 扩展业务属性。

// 自定义一个 bizErr
type bizErr struct {
   code int
   msg  string
}

func NewBizErr(code int, msg string) *bizErr {
   return &bizErr{code: code, msg: msg}
}
// 实现 Error 方法
func (be *bizErr) Error() string {
   return be.msg
}

func (be *bizErr) Code() int {
   return be.code
}

func service() (int, error) {
   return 0, NewBizErr(1000, "biz err")
}

func main() {
   _, err := service()
   be := NewBizErr(0, "")
   // 获取 errCode, 使用时会封装到中间件里
   if errors.As(err, &be) {
      fmt.Println(be.Code(), ":", be.Error())
   }
}

error Wrap

目前, 可以通过自定义 bizErr 实现对业务的支持。但是在业务场景中, 往往涉及到函数的层层调用, 仅仅通过 msg 无法表述错误是从哪里开始一层一层向上层传递的。可以借助 github.com/pkg/errors 包来处理这一问题。

var ErrDB = NewBizErr(10000, "db err")

// 使用 github.com/pkg/errors 创建带有 stack 的 err 信息
func db() error {
   return errors.WithStack(ErrDB)
}

func service() (int, error) {
   if errDB := db(); errDB != nil {
      // 包装 msg
      return 0, errors.WithMessage(errDB, "service err:")
   }
   return 0, nil
}

func main() {
   _, err := service()
   // 是否是业务定义的 DB 错误
   if errors.Is(err, ErrDB) {
      fmt.Println("main -> db err")
   }
   be := &bizErr{}
   // 获取调用链上的业务错误
   if errors.As(err, &be) {
      fmt.Println("main -> err_code: ", be.Code())
   }
   // 打印错误堆栈
   fmt.Printf("main -> err: %+v", err)
}

总结

在 go 语言中,本质上是利用多值返回解决错误处理问题。即在函数返回时,额外返回一个 err。在函数调用时需要先判断 err。err 本质上是一个接口类型,任何实现 err 接口的类型都可以被当作一个 err。在具体的业务实践中,通常会选择自定义的 err,来保证错误逻辑的展示。