在 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,来保证错误逻辑的展示。