Golang项目中error错误处理最佳实践

446 阅读3分钟

需求分析

在golang项目中,error处理是非常常见的,但golang本身提供的error接口比较简单,仅仅是返回错误信息。我们想要的是:

  1. 能给每个error一个唯一标识,然后在项目中定义一些常见的、通用的error供大家使用
  2. 能对error进行特判,当遇到的error是某特殊error时,可以做一些特殊处理
  3. 使用定义好的通用error时,能重写error描述信息,从而返回符合当前具体业务场景的错误描述

最佳实践

对golang提供的error接口进行扩展,实现符合自己要求的error,然后在项目中统一用该error。如下:

// ErrorIdentify 扩展error接口,带唯一标识。
type ErrorIdentify interface {
   // 接口继承
   error
   // ID 返回error的唯一标识
   ID() int32
}

// IError 再次扩展ErrorIdentify,扩展其他能力
type IError interface {
   // ErrorIdentify 接口继承
   ErrorIdentify

   // Name error名称
   Name() string
   // Message error简单描述
   Message() string
   // Unwrap 返回包装的error
   Unwrap() error
   // Errorf 提供重写error错误信息的能力,返回一个新error(避免改变原本的error)
   Errorf(format string, a ...interface{}) IError
}

// bizError 实现IError接口
type bizError struct {
   ErrID   int32
   ErrName string
   Msg     string
   wrapErr error
   // ErrorMsgList 用于包装error的多个错误描述,有些场景用户需要重写一个error的描述,多次重写的描述通过这个字段承载
   ErrorMsgList []string
}

func NewBizError(id int32, name string, msg string) *bizError {
   return &bizError{
      ErrID:   id,
      ErrName: name,
      Msg:     msg,
   }
}

func (b *bizError) ID() int32 {
   return b.ErrID
}

func (b *bizError) Name() string {
   return b.ErrName
}

func (b *bizError) Message() string {
   return b.Msg
}

func (b *bizError) Unwrap() error {
   return b.wrapErr
}

func (b *bizError) Errorf(format string, a ...interface{}) IError {
   // 之所以clone,是为了避免改变原本的error
   newBizError := b.clone()
   err0 := fmt.Errorf(format, a...)
   if newBizError.wrapErr == nil {
      newBizError.wrapErr = err0
   }

   // append重写的error信息
   newBizError.ErrorMsgList = append(newBizError.ErrorMsgList, err0.Error())
   return newBizError
}

func (b *bizError) clone() *bizError {
   return &bizError{
      ErrID:        b.ErrID,
      ErrName:      b.ErrName,
      Msg:          b.Msg,
      wrapErr:      b.wrapErr,
      ErrorMsgList: b.ErrorMsgList,
   }
}

// Error 返回error信息
func (b *bizError) Error() string {
   return fmt.Sprintf("id=%d, name=%s, msg=%s, wrapErr=[%v], errorMsgList=[%v]",
      b.ID(), b.Name(), b.Message(), b.Unwrap(), strings.Join(b.ErrorMsgList, "|"))
}

// Is 比较error是否相等
func (b *bizError) Is(err error) bool {
   err0, ok := err.(*bizError)
   if ok {
      // 通过ID比较相等
      return err0.ErrID == b.ErrID
   }
   return false
}

测试:

// 预定义一些常用、通用的error供大家使用
var (
   ErrTest1 = NewBizError(1, "TestErr1", "test1 msg")
   ErrTest2 = NewBizError(2, "TestErr2", "test2 msg")
)

func TestMyError(t *testing.T) {
   err1 := mockProcess1()
   if errors.Is(err1, ErrTest1) {
      t.Logf("mockProcess1 return ErrTest1, err1:%v", err1)
   }

   err2 := mockProcess2()
   if errors.Is(err2, ErrTest2) {
      t.Logf("mockProcess2 return ErrTest2, err2:%v", err2)
   }

   err3 := mockProcess3()
   if errors.Is(err3, ErrTest1) {
      t.Logf("mockProcess3 return ErrTest1, err3:%v", err3)
   }
}

// mockProcess1 模拟业务逻辑处理
func mockProcess1() error {
   // process....

   // 模拟报错
   return ErrTest1
}

// mockProcess2 模拟业务逻辑处理
func mockProcess2() error {
   // process....

   // 模拟报错
   return ErrTest2
}

// mockProcess3 模拟业务逻辑处理
func mockProcess3() error {
   // process....

   // 模拟报错,需要自定义错误信息:返回符合当前业务场景的错误信息
   return ErrTest1.Errorf("detail: %s", "error detail info")
}

运行结果:

=== RUN   TestMyError
    my_error_test.go:17: mockProcess1 return ErrTest1, err1:id=1, name=TestErr1, msg=test1 msg, wrapErr=[<nil>], errorMsgList=[]
    my_error_test.go:22: mockProcess2 return ErrTest2, err2:id=2, name=TestErr2, msg=test2 msg, wrapErr=[<nil>], errorMsgList=[]
    my_error_test.go:27: mockProcess3 return ErrTest1, err3:id=1, name=TestErr1, msg=test1 msg, wrapErr=[detail: error detail info], errorMsgList=[detail: error detail info]
--- PASS: TestMyError (0.00s)
PASS

感悟

学会面向接口编程,使用接口定义协议,然后实现协议。
学会继承已有接口,在已有接口的基础上扩展其他能力