在Go中到底该如何处理错误呢?到底有没有什么最佳实践呢? panic和error到底都在什么情况下使用? 下面我们就来具体说说在Go中应该如何处理错误。

最佳实践总结

panic

  • 在程序启动的时候,如果有强依赖的服务出现故障时 panic 退出, 比如启动MySQL、Redis等
  • 在程序启动的时候,如果发现有配置明显不符合要求, 可以 panic 退出(防御编程)
  • 其他情况下只要不是不可恢复的程序错误,都不应该直接 panic 应该返回 error
  • 在程序入口处,例如 gin 中间件需要使用 recover 预防 panic 程序退出
  • 在程序中我们应该避免使用野生的 goroutine
    • 如果是在请求中需要执行异步任务,应该使用异步 worker ,消息通知的方式进行处理,避免请求量大时大量 goroutine 创建
    • 如果需要使用 goroutine 时,应该使用同一的 Go 函数进行创建,这个函数中会进行 recover ,避免因为野生 goroutine panic 导致主进程退出
  1. func Go(f func()){
  2. go func(){
  3. defer func(){
  4. if err := recover(); err != nil {
  5. log.Printf("panic: %+v", err)
  6. }
  7. }()
  8. f()
  9. }()
  10. }

error

  • 在应用程序中使用 github.com/pkg/errors 处理应用错误,注意:在公共库当中,一般不建议使用这个
  • error 应该是函数的最后一个返回值,当 error 不为 nil 时,函数的其他返回值是不可用的状态,不应该对其他返回值做任何期待
  • 错误处理的时候应该先判断错误, if err != nil 出现错误及时返回,使代码是一条流畅的直线,避免过多的嵌套. 例如: ```go // good case func f() error { a, err := A() if err != nil {

    1. return err

    }

    // … 其他逻辑 return nil }

// bad case func f() error { a, err := A() if err == nil { // 其他逻辑 }

  1. return err

}

  1. - 在应用程序中出现错误时,使用 `errors.New` 或者 `errors.Errorf` 返回错误
  2. ```go
  3. func (u *usecese) usecase1() error {
  4. money := u.repo.getMoney(uid)
  5. if money < 10 {
  6. errors.Errorf("用户余额不足, uid: %d, money: %d", uid, money)
  7. }
  8. // 其他逻辑
  9. return nil
  10. }
  • 如果是调用应用程序的其他函数出现错误,请直接返回,如果需要携带信息,请使用 errors.WithMessage

    1. func (u *usecese) usecase2() error {
    2. name, err := u.repo.getUserName(uid)
    3. if err != nil {
    4. return errors.WithMessage(err, "其他附加信息")
    5. }
    6. // 其他逻辑
    7. return nil
    8. }
  • 如果是调用其他库(标准库、企业公共库、开源第三方库等)获取到错误时,请使用 errors.Wrap 添加堆栈信息

    • 切记,不要每个地方都是用 errors.Wrap 只需要在错误第一次出现时进行 errors.Wrap 即可
    • 根据场景进行判断是否需要将其他库的原始错误吞掉,例如可以把 repository 层的数据库相关错误吞掉,返回业务错误码,避免后续我们分割微服务或者更换 ORM 库时需要去修改上层代码
    • 注意我们在基础库,被大量引入的第三方库编写时一般不使用 errors.Wrap 避免堆栈信息重复

      1. func f() error {
      2. err := json.Unmashal(&a, data)
      3. if err != nil {
      4. return errors.Wrap(err, "其他附加信息")
      5. }
      6. // 其他逻辑
      7. return nil
      8. }
  • 禁止每个出错的地方都打日志,只需要在进程的最开始的地方使用 %+v 进行统一打印,例如 http/rpc 服务的中间件

  • 错误判断使用 errors.Is 进行比较

    1. func f() error {
    2. err := A()
    3. if errors.Is(err, io.EOF){
    4. return nil
    5. }
    6. // 其他逻辑
    7. return nil
    8. }
  • 错误类型判断,使用 errors.As 进行赋值

    1. func f() error {
    2. err := A()
    3. var errA errorA
    4. if errors.As(err, &errA){
    5. // ...
    6. }
    7. // 其他逻辑
    8. return nil
    9. }
  • 如何判定错误的信息是否足够,想一想当你的代码出现问题需要排查的时候你的错误信息是否可以帮助你快速的定位问题,例如我们在请求中一般会输出参数信息,用于辅助判断错误

  • 对于业务错误,推荐在一个统一的地方创建一个错误字典,错误字典里面应该包含错误的 code,并且在日志中作为独立字段打印,方便做业务告警的判断,错误必须有清晰的错误文档
  • 不需要返回,被忽略的错误必须输出日志信息
  • 同一个地方不停的报错,最好不要不停输出错误日志,这样可能会导致被大量的错误日志信息淹没,无法排查问题,比较好的做法是打印一次错误详情,然后打印出错误出现的次数
  • 对同一个类型的错误,采用相同的模式,例如参数错误,不要有的返回 404 有的返回 200
  • 处理错误的时候,需要处理已分配的资源,使用 defer 进行清理,例如文件句柄

到底是用 panic or error ?

  1. 在 Go 中 panic 会导致程序直接退出,是一个致命的错误,如果使用 panic recover 进行处理的话,会存在很多问题
    • 性能问题,频繁 panic recover 性能不好
    • 容易导致程序异常退出,只要有一个地方没有处理到就会导致程序进程整个退出
    • 不可控,一旦 panic 就将处理逻辑移交给了外部,我们并不能预设外部包一定会进行处理
  2. 什么时候使用 panic 呢?
    • 对于真正意外的情况,那些表示不可恢复的程序错误,例如索引越界不可恢复的环境问题、栈溢出,我们才使用 panic
  3. 使用 error 处理有哪些好处?
    • 简单
    • 考虑失败,而不是成功(Plan for failure, not success)
    • 没有隐藏的控制流
    • 完全交给你来控制 error
    • Error are values