Go error如何优雅的处理?

208 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 20天,点击查看活动详情

前言

PHP处理程序异常错误是通过try/catch机制来进行捕获的,到学Go的时候发现Go并没有这个机制,而是通过定义error接口类型,来明确返回错误信息这个特殊值,从而进一步处理。

下面先快速过一遍error的基础知识,然后进一步学习如何优雅的处理error。

基础知识

error接口定义

type error interface {
  Error() string
}

创建error

使用errors.New方法,输入一个错误信息文本,输出一个error接口类型,可使用err.Error获取错误文本

func main() {
  err := errors.New("error here")
  if err != nil {
    fmt.Println(err) //这里底层会以err.Error()去执行,获取到错误文本并打印
    return
  }
  fmt.Println("good")
}

//输出:
//error here

errors.New的实现

只要实现了Error方法,就可以认为是实现了这个error接口

package errors

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
  return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
  s string
}

func (e *errorString) Error() string {
  return e.s
}

fmt.Errorf包装error

使用%w%v,进行包装错误。注意:输出的类型有所区别( %w是包装之后的error,%v是原始的error

func main() {
  err := errors.New("here is error")
  if err != nil {
    fmt.Println(err)
    err2 := fmt.Errorf("format err:%w", err)
    fmt.Printf("type: %T, val: %v", err2, err2) //type: *fmt.wrapError, val: format err:here is error
    fmt.Println()
    err3 := fmt.Errorf("format err:%v", err)
    fmt.Printf("type: %T, val: %v", err3, err3) //type: *errors.errorString, val: format err:here is error
    return
  }
  fmt.Println("good")
}

errors包的其他函数

针对包装过的error,erros包还提供了下面3个方法

func Unwrap(err error) error                 // 获得err包含下一层错误
func Is(err, target error) bool              // 判断err是否包含target
func As(err error, target interface{}) bool  // 判断err是否为target类型

优雅的处理error

Bad Case

遇到err就打日志,并直接把err一层一层往上抛,这样会导致最终在哪报的err都不明确。日志冗余的同时,排查问题的难度也被加大

func main() {
  err := do1()
  fmt.Println(err)
}

func do1() error {
  err := do2()
  if err != nil {
    log.Printf("do1 err:%v", err)
    return err
  }
  return nil
}

func do2() error {
  _, err := os.OpenFile("./a.go", os.O_RDONLY, 0)
  if err != nil {
    log.Printf("do2 err:%v", err)
    return err
  }
  return nil
}

//输出:
//2023/02/21 20:34:39 do2 err:open ./a.go: no such file or directory
//2023/02/21 20:34:39 do1 err:open ./a.go: no such file or directory
//open ./a.go: no such file or directory

Good Case

使用errros.Wrap,做error的包装,也不需要每一步err都打日志,因为有明确的调用栈stack,能很明确的知道报错的地方以及操作链路

func main() {
  err := do11()
  fmt.Println(err)
  fmt.Printf("origin error: %T %v\n\n", errors.Cause(err), errors.Cause(err))
  fmt.Printf("stack trace: %+v\n", err)
}

func do11() error {
  err := do12()
  if err != nil {
    return errors.Wrap(err, "do11 error")
  }
  return nil
}

func do12() error {
  _, err := os.OpenFile("./a.go", os.O_RDONLY, 0)
  if err != nil {
    return errors.Wrap(err, "do12 error")
  }
  return nil
}

输出结果

do11 error: do12 error: open ./a.go: no such file or directory
origin error: *fs.PathError open ./a.go: no such file or directory

stack trace: open ./a.go: no such file or directory
do12 error
main.do12
  /usr/local/var/www_go/src2/learn-go/09_error/03_error_wrap.go:30
main.do11
  /usr/local/var/www_go/src2/learn-go/09_error/03_error_wrap.go:20
main.main
  /usr/local/var/www_go/src2/learn-go/09_error/03_error_wrap.go:12
runtime.main
  /usr/local/go/src/runtime/proc.go:225
runtime.goexit
  /usr/local/go/src/runtime/asm_amd64.s:1371
do11 error
main.do11
  /usr/local/var/www_go/src2/learn-go/09_error/03_error_wrap.go:22
main.main
  /usr/local/var/www_go/src2/learn-go/09_error/03_error_wrap.go:12
runtime.main
  /usr/local/go/src/runtime/proc.go:225
runtime.goexit
  /usr/local/go/src/runtime/asm_amd64.s:1371

总结

从PHP的try/catch机制到Go的error接口类型特殊值,对程序异常错误的处理,编码思维发生了彻底的变化。针对Go error如果优雅的处理,做了Bad Case和Good Case的对比,明确知道了使用errors.Wrap写法能更加优雅。

如果本文对你有帮助,欢迎点赞收藏加关注,如果本文有错误的地方,欢迎指出!