文章转自:Go error 处理最佳实践

Go error 处理最佳实践

今天分享 go 语言 error 处理的最佳实践,了解当前 error 的缺点、妥协以及使用时注意事项。文章内容较长,干货也多,建义收藏

什么是 error

大家都知道 error[1] 是源代码内嵌的接口类型。根据导出原则,只有大写的才能被其它源码包引用,但是 error 属于 predeclared identifiers 预定义的,并不是关键字,细节参考int make 居然不是关键字?

// 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 只有一个方法 Error() string 返回错误消息

// 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
}

一般我们创建 error 时只需要调用 errors.New("error from somewhere") 即可,底层就是一个字符串结构体 errorStrings

当前 error 有哪些问题

func Test() error {if err := func1(); err != nil {return err}......
}

这是常见的用法,也最被人诟病,很多人觉得不如 try-catch 用法简洁,有人戏称 go 源码错误处理占一半

import systry:f = open('myfile.txt')s = f.readline()i = int(s.strip())
except OSError as err:print("OS error: {0}".format(err))
except ValueError:print("Could not convert data to an integer.")
except BaseException as err:print(f"Unexpected {err=}, {type(err)=}")raise

比如上面是 python try-catch 的用法,先写一堆逻辑,不处理异常,最后统一捕获

let mut cfg = self.check_and_copy()?;

相比来说 rust Result 模式更简洁,一个 ? 就代替了我们的操作。但是 error 的繁琐判断是当前的痛点嘛?显然不是,尤其喜欢 c 语言的人,反而喜欢每次都做判断

在我看来 go 的痛点不是缺少泛型,不是 error 太挫,而是 GC 太弱,尤其对大内存非常不友好,这方面可以参考真实环境下大内存 Go 服务性能优化一例

当前 error 的问题有两点:

  1. 无法 wrap 更多的信息,比如调用栈,比如层层封装的 error 消息

  2. 无法很好的处理类型信息,比如我想知道错误是 io 类型的,还是 net 类型的

1.Wrap 更多的消息

这方面有很多轮子,最著名的就是 https://github.com/pkg/errors, 我司也重度使用,主要功能有三个:

  1. Wrap 封装底层 error, 增加更多消息,提供调用栈信息,这是原生 error 缺少的

  2. WithMessage 封装底层 error, 增加更多消息,但不提供调用栈信息

  3. Cause 返回最底层的 error, 剥去层层的 wrap

import ("database/sql""fmt""github.com/pkg/errors"
)func foo() error {return errors.Wrap(sql.ErrNoRows, "foo failed")
}func bar() error {return errors.WithMessage(foo(), "bar failed")
}func main() {err := bar()if errors.Cause(err) == sql.ErrNoRows {fmt.Printf("data not found, %v\n", err)fmt.Printf("%+v\n", err)return}if err != nil {// unknown error}
}
/*Output:
data not found, bar failed: foo failed: sql: no rows in result set
sql: no rows in result set
foo failed
main.foo/usr/three/main.go:11
main.bar/usr/three/main.go:15
main.main/usr/three/main.go:19
runtime.main...
*/

这是测试代码,当用 %v 打印时只有原始错误信息,%+v 时打印完整调用栈。当 go1.13 后,标准库 errors 增加了 Wrap 方法

func ExampleUnwrap() {err1 := errors.New("error1")err2 := fmt.Errorf("error2: [%w]", err1)fmt.Println(err2)fmt.Println(errors.Unwrap(err2))// Output// error2: [error1]// error1
}

标准库没有提供增加调用栈的方法,fmt.Errorf 指定 %w 时可以 wrap error, 但整体来讲,并没有 https://github.com/pkg/errors 库好用

2.错误类型

这个例子来自 ITNEXT[2]

import ("database/sql""fmt"
)func foo() error {return sql.ErrNoRows
}func bar() error {return foo()
}func main() {err := bar()if err == sql.ErrNoRows {fmt.Printf("data not found, %+v\n", err)return}if err != nil {// Unknown error}
}
//Outputs:
// data not found, sql: no rows in result set

有时我们要处理类型信息,比如上面例子,判断 err 如果是 sql.ErrNoRows 那么视为正常,data not found 而己,类似于 redigo 里面的 redigo.Nil 表示记录不存在

func foo() error {return fmt.Errorf("foo err, %v", sql.ErrNoRows)
}

但是如果 foo 把 error 做了一层 wrap 呢?这个时候错误还是 sql.ErrNoRows 嘛?肯定不是,这点没有 python try-catch 错误处理强大,可以根据不同错误 class 做出判断。那么 go 如何解决呢?答案是 go1.13 新增的 Is[3] 和 As

import ("database/sql""errors""fmt"
)func bar() error {if err := foo(); err != nil {return fmt.Errorf("bar failed: %w", foo())}return nil
}func foo() error {return fmt.Errorf("foo failed: %w", sql.ErrNoRows)
}func main() {err := bar()if errors.Is(err, sql.ErrNoRows) {fmt.Printf("data not found,  %+v\n", err)return}if err != nil {// unknown error}
}
/* Outputs:
data not found,  bar failed: foo failed: sql: no rows in result set
*/

还是这个例子,errors.Is 会递归的 Unwrap err, 判断错误是不是 sql.ErrNoRows,这里个小问题,Is 是做的指针地址判断,如果错误 Error() 内容一样,但是根 error 是不同实例,那么 Is 判断也是 false, 这点就很扯

func ExampleAs() {if _, err := os.Open("non-existing"); err != nil {var pathError *fs.PathErrorif errors.As(err, &pathError) {fmt.Println("Failed at path:", pathError.Path)} else {fmt.Println(err)}}// Output:// Failed at path: non-existing
}

errors.As[4] 判断这个 err 是否是 fs.PathError 类型,递归调用层层查找,源码后面再讲解

另外一个判断类型或是错误原因的就是 https://github.com/pkg/errors 库提供的 errors.Cause

switch err := errors.Cause(err).(type) {
case *MyError:// handle specifically
default:// unknown error
}

在没有 Is As 类型判断时,需要很恶心的去判断错误自符串

func (conn *cendolConnectionV5) serve() {// Buffer needs to be preserved across messages because of packet coalescing.reader := bufio.NewReader(conn.Connection)for {msg, err := conn.readMessage(reader)if err != nil {if netErr, ok := strings.Contain(err.Error(), "temprary"); ok   {continue}}conn.processMessage(msg)}
}

想必接触 go 比较早的人一定很熟悉,如果 conn 从网络接受到的连接错误是 temporary 临时的那么可以 continue 重试,当然最好 backoff sleep 一下

当然现在新增加了 net.Error 类型,实现了 Temporary 接口,不过也要废弃了,请参考#45729[5]

源码实现

1.github.com/pkg/errors 库如何生成 warapper error

// Wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
// If err is nil, Wrap returns nil.
func Wrap(err error, message string) error {if err == nil {return nil}err = &withMessage{cause: err,msg:   message,}return &withStack{err,callers(),}
}

主要的函数就是 Wrap, 代码实现比较简单,查看如何追踪调用栈可以查看源码

2.github.com/pkg/errors 库 Cause 实现

type withStack struct {error*stack
}func (w *withStack) Cause() error { return w.error }func Cause(err error) error {type causer interface {Cause() error}for err != nil {cause, ok := err.(causer)if !ok {break}err = cause.Cause()}return err
}

Cause 递归调用,如果没有实现 causer 接口,那么就返回这个 err

3.官方库如何生成一个 wrapper error

官方没有这样的函数,而是 fmt.Errorf 格式化时使用 %w

e := errors.New("this is a error")
w := fmt.Errorf("more info about it %w", e)
func Errorf(format string, a ...interface{}) error {p := newPrinter()p.wrapErrs = truep.doPrintf(format, a)s := string(p.buf)var err errorif p.wrappedErr == nil {err = errors.New(s)} else {err = &wrapError{s, p.wrappedErr}}p.free()return err
}func (p *pp) handleMethods(verb rune) (handled bool) {if p.erroring {return}if verb == 'w' {// It is invalid to use %w other than with Errorf, more than once,// or with a non-error arg.err, ok := p.arg.(error)if !ok || !p.wrapErrs || p.wrappedErr != nil {p.wrappedErr = nilp.wrapErrs = falsep.badVerb(verb)return true}p.wrappedErr = err// If the arg is a Formatter, pass 'v' as the verb to it.verb = 'v'}......
}

代码也不难,handleMethods 时特殊处理 w, 使用 wrapError 封装一下即可

4.官方库 Unwrap 实现

func Unwrap(err error) error {u, ok := err.(interface {Unwrap() error})if !ok {return nil}return u.Unwrap()
}

也是递归调用,否则接口断言失败,返回 nil

type wrapError struct {msg stringerr error
}func (e *wrapError) Error() string {return e.msg
}func (e *wrapError) Unwrap() error {return e.err
}

上文 fmt.Errof 时生成的 error 结构体如上所示,Unwrap 直接返回底层 err

5.官方库 Is As 实现

本段源码分析来自 flysnow[6]

func Is(err, target error) bool {if target == nil {return err == target}isComparable := reflectlite.TypeOf(target).Comparable()//for循环,把err一层层剥开,一个个比较,找到就返回truefor {if isComparable && err == target {return true}//这里意味着你可以自定义error的Is方法,实现自己的比较代码if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {return true}//剥开一层,返回被嵌套的errif err = Unwrap(err); err == nil {return false}}
}

Is 函数比较简单,递归层层检查,如果是嵌套 err, 那就调用 Unwrap 层层剥开找到最底层 err, 最后判断指针是否相等

var errorType = reflectlite.TypeOf((*error)(nil)).Elem()func As(err error, target interface{}) bool {//一些判断,保证target,这里是不能为nilif target == nil {panic("errors: target cannot be nil")}val := reflectlite.ValueOf(target)typ := val.Type()//这里确保target必须是一个非nil指针if typ.Kind() != reflectlite.Ptr || val.IsNil() {panic("errors: target must be a non-nil pointer")}//这里确保target是一个接口或者实现了error接口if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) {panic("errors: *target must be interface or implement error")}targetType := typ.Elem()for err != nil {//关键部分,反射判断是否可被赋予,如果可以就赋值并且返回true//本质上,就是类型断言,这是反射的写法if reflectlite.TypeOf(err).AssignableTo(targetType) {val.Elem().Set(reflectlite.ValueOf(err))return true}//这里意味着你可以自定义error的As方法,实现自己的类型断言代码if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {return true}//这里是遍历error链的关键,不停的Unwrap,一层层的获取errerr = Unwrap(err)}return false
}

代码同样是递归调用 As, 同时 Unwrap 最底层的 error, 然后用反射判断是否可以赋值,如果可以,那么说明是同一类型

ErrGroup 使用

提到 error 就必须要提一下 golang.org/x/sync/errgroup, 适用如下场景:并发场景下,如果一个 goroutine 有错误,那么就要提前返回,并取消其它并行的请求

func ExampleGroup_justErrors() {g := new(errgroup.Group)var urls = []string{"http://www.golang.org/","http://www.google.com/","http://www.somestupidname.com/",}for _, url := range urls {// Launch a goroutine to fetch the URL.url := url // https://golang.org/doc/faq#closures_and_goroutinesg.Go(func() error {// Fetch the URL.resp, err := http.Get(url)if err == nil {resp.Body.Close()}return err})}// Wait for all HTTP fetches to complete.if err := g.Wait(); err == nil {fmt.Println("Successfully fetched all URLs.")}
}

上面是官方给的例子,底层使用 context 来 cancel 其它请求,同步使用 WaitGroup, 原理非常简单,代码量非常少,感兴趣的可以看源码

这里一定要注意三点:

  1. context 是谁传进来的?其它代码会不会用到,cancel 只能执行一次,瞎比用会出问题

  2. g.Go 不带 recover 的,为了程序的健壮,一定要自行 recover

  3. 并行的 goroutine 有一个错误就返回,而不是普通的 fan-out 请求后收集结果

线上实践注意的几个问题

1.error 与 panic

查看 go 源代码会发现,源码很多地方写 panic, 但是工程实践,尤其业务代码不要主动写 panic

理论上 panic 只存在于 server 启动阶段,比如 config 文件解析失败,端口监听失败等等,所有业务逻辑禁止主动 panic

根据 CAP 理论,当前 web 互联网最重要的是 AP, 高可用性才最关键(非银行金融场景),程序启动时如果有部分词表,元数据加载失败,都不能 panic, 提供服务才最关键,当然要有报警,让开发第一时间感知当前服务了的 QOS 己经降低

最后说一下,所有异步的 goroutine 都要用 recover 去兜底处理

2.错误处理与资源释放

func worker(done chan error) {err := doSomething()result := &result{}if err != nil {result.Err = err}done <- result
}

一般异步组装数据,都要分别启动 goroutine, 然后把结果通过 channel 返回,result 结构体拥有 err 字段表示错误

这里要注意,main 函数中 done channel 千万不能 close, 因为你不知道 doSomething 会超时多久返回,写 closed channel 直接 panic

所以这里有一个准则:数据传输和退出控制,需要用单独的 channel 不能混, 我们一般用 context 取消异步 goroutine, 而不是直接 close channels

3.error 级联使用问题

package mainimport "fmt"type myError struct {string
}func (i *myError) Error() string {return i.string
}func Call1() error {return nil
}func Call2() *myError {return nil
}func main() {err := Call1()if err != nil {fmt.Printf("call1 is not nil: %v\n", err)}err = Call2()if err != nil {fmt.Printf("call2 err is not nil: %v\n", err)}
}

这个问题非常经典,如果复用 err 变量的情况下, Call2 返回的 error 是自定义类型,此时 err 类型是不一样的,导致经典的 error is not nil, but value is nil

非常经典的 Nil is not nil[7] 问题。解决方法就是 Call2 err 重新定义一个变量,当然最简单就是统一 error 类型。有点难,尤其是大型项目

4.并发问题

go 内置类型除了 channel 大部分都是非线程安全的,error 也不例外,先看一个例子

package main
import ("fmt""github.com/myteksi/hystrix-go/hystrix""time"
)
var FIRST error = hystrix.CircuitError{Message:"timeout"}
var SECOND error = nil
func main() {var err errorgo func() {i := 1for {i = 1 - iif i == 0 {err = FIRST} else {err = SECOND}time.Sleep(10)}}()for {if err != nil {fmt.Println(err.Error())}time.Sleep(10)}
}

运行之前,大家先猜下会发生什么???

zerun.dong$ go run panic.go
hystrix: timeout
panic: value method github.com/myteksi/hystrix-go/hystrix.CircuitError.Error called using nil *CircuitError pointergoroutine 1 [running]:
github.com/myteksi/hystrix-go/hystrix.(*CircuitError).Error(0x0, 0xc0000f4008, 0xc000088f40)<autogenerated>:1 +0x86
main.main()/Users/zerun.dong/code/gotest/panic.go:25 +0x82
exit status 2

上面是测试的例子,只要跑一会,就一定发生 panic, 本质就是 error 接口类型不是并发安全的

// 没有方法的interface
type eface struct {_type *_typedata  unsafe.Pointer
}
// 有方法的interface
type iface struct {tab  *itabdata unsafe.Pointer
}

所以不要并发对 error 赋值

5.error 要不要忽略

func Test(){_ = json.Marshal(xxxx)......
}

有的同学会有疑问,error 是否一定要处理?其实上面的 Marshal 都有可能失败的

如果换成其它函数,当前实现可以忽略,不能保证以后还是兼容的逻辑,一定要处理 error,至少要打日志

6.errWriter

本例来自官方 blog[8], 有时我们想做 pipeline 处理,需要把 err 当成结构体变量

_, err = fd.Write(p0[a:b])
if err != nil {return err
}
_, err = fd.Write(p1[c:d])
if err != nil {return err
}
_, err = fd.Write(p2[e:f])
if err != nil {return err
}
// and so on

上面是原始例子,需要一直做 if err != nil 的判断,官方优化的写法如下

type errWriter struct {w   io.Writererr error
}func (ew *errWriter) write(buf []byte) {if ew.err != nil {return}_, ew.err = ew.w.Write(buf)
}// 使用时
ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {return ew.err
}

清晰简洁,大家平时写代码可以多考滤一下

7.何时打印调用栈

官方库无法 wrap 调用栈,所以 fmt.Errorf %w 不如 pkg/errors 库实用,但是errors.Wrap 最好保证只调用一次,否则全是重复的调用栈

我们项目的使用情况是 log error 级别的打印栈,warn 和 info 都不打印,当然 case by case 还得看实际使用情况

8.Wrap前做判断

errors.Wrap(err, "failed")

通过查看源码,如果 err 为 nil 的时候,也会返回 nil. 所以 Wrap 前最好做下判断,建议来自 xiaorui.cc

小结

上面提到的线上实践注意的几个问题,都是实际发生的坑,惨痛的教训,大家一定要多体会下。错误处理涵盖内容非常广,本文不涉及分布式系统的错误处理、gRPC 错误传播以及错误管理

写文章不容易,如果对大家有所帮助和启发,请大家帮忙点击在看点赞分享 三连

关于 error 大家有什么看法,欢迎留言一起讨论,大牛多留言 ^_^

参考资料

[1]

builting.go error interface: https://github.com/golang/go/blob/master/src/builtin/builtin.go#L260,

[2]

ITNEXT: https://itnext.io/golang-error-handling-best-practice-a36f47b0b94c,

[3]

errors.Is: https://github.com/golang/go/blob/master/src/errors/wrap.go#L40,

[4]

errors.As example: https://github.com/golang/go/blob/master/src/errors/wrap_test.go#L255,

[5]

#45729: https://github.com/golang/go/issues/45729,

[6]

flysnow error 分析: https://www.flysnow.org/2019/09/06/go1.13-error-wrapping.html,

[7]

Nil is not nil: https://yourbasic.org/golang/gotcha-why-nil-error-not-equal-nil/,

[8]

errors are values: https://blog.golang.org/errors-are-values,

Go error 处理实践相关推荐

  1. Deep-Learning-YOLOV4实践:ScaledYOLOv4模型训练自己的数据集调试问题总结

    error error1: CUDA out of memory error2:TypeError: can't convert cuda: error Deep-Learning-YOLOV4实践: ...

  2. Jetson部署实践

    Jetson部署实践 一.模型开发 1.1 Yolov5图像检测模型开发 二.模型部署 2.1 DeepStream框架介绍 2.2 TensorRT 加速算子.模型转换 2.3 安装DeepStre ...

  3. 浅谈 GO 语言错误处理

    go 的异常处理一直都是一种让人感觉奇怪的设计,本文用较多的篇幅和大家一起聊聊go 的异常处理的一些姿势 一.error 是什么玩意 话不多说 ,先放下源码(也就几行) package builtin ...

  4. 我爱机器学习网机器学习类别文章汇总

    机器学习领域的几种主要学习方式 From Stumps to Trees to Forests KDD-2014 – The Biggest, Best, and Booming Data Scien ...

  5. 我爱机器学习--机器学习方向资料汇总

    转载:http://blog.csdn.net/shuimanting520/article/details/45748505 机器学习爱好者资料 机器学习领域的几种主要学习方式 From Stump ...

  6. load xml error什么意思_XML文件解析实践(DOM解析)

    昨天完成了基于DOM的XML文件解析类,今天赶紧实践了一下,不得不说,实践中的坑还是很多的. 本来这个项目就是为了规范各个服务在使用MySQL数据库时候的配置项,由于之前我接触的都是Java服务,对于 ...

  7. Go error 处理最佳实践

    今天分享 go 语言 error 处理的最佳实践,了解当前 error 的缺点.妥协以及使用时注意事项.文章内容较长,干货也多,建议收藏 什么是 error 大家都知道 error[1] 是源代码内嵌 ...

  8. 〖ChatGPT实践指南 - 零基础扫盲篇⑥〗- OpenAI API 报错An error occurred during your request

    帮助大家学习使用OpenAI的各类API开发应用 ,学习多个实站项目. 推荐他人订阅可获取扣除平台费用后的35%收益,文末有名片! 说明:该文属于 ChatGPT实践指南白宝书 专栏,购买任意白宝书体 ...

  9. ‘adb‘ 不是内部或外部命令和Error while executing: am start -n的解决实践

    文章目录 前言 一.解决方法 1.引用参考,感谢前辈!! 2. 解决adb不是内部或外部命令 2.1.1. 点击该按钮 2.1.2. 打开上面文件夹 2.1.3. 验证是否可以 3. 解决Error ...

最新文章

  1. 吴明曦:马斯克的天基互联网与未来6G地基互联网优劣比较分析
  2. SqlServer数据库基础知识整理(不断更新~)
  3. java 之绘图技术
  4. geth bootnodes
  5. cpld xilinx 定义全局时钟_时钟相关概念
  6. 【SMTP 补录 Apache服务】
  7. SurvivalShooter学习笔记(八.敌人管理器)
  8. SQL分组求每组最大值问题的解决方法收集
  9. CI/CD(持续集成构建/持续交付):如何测试/集成/交付项目代码?(Jenkins,TravisCI)
  10. CentOS 7.X配置连接网络
  11. python股票交易模型_利用python建立股票量化交易系统(一)——小市值选股票模型...
  12. html 漂浮 广告置顶,jquery浮动图片广告代码_页面上漂浮图片广告代码
  13. [na]win7系统安装在t450s
  14. Delphi程序破解技术概要
  15. 一张“黑洞”照片需半吨重硬盘?更逆天的操作还有这些……
  16. PCF应用管理平台介绍(PCF Apps Manager)
  17. linux开机启动出现grup,开机出现grub解决方法
  18. Hexo个人博客搭建教程
  19. 【转】Thunderbird on Ubuntu 12.04 – 调整邮件列表行间距
  20. java设计模式——门面与调停

热门文章

  1. java高效获取大文件的行数
  2. iPhone 诈骗又出新招,别看见弹窗就输密码
  3. 作业6--四则运算APP之Sprint计划
  4. 基于jetty9 编程构建嵌入式https 服务器
  5. C#编程指南:使用属性
  6. 在路由器与交换机之间添加ISA Server软路由与防火墙
  7. 应用程序架构指导袖珍版
  8. Python 基础学习 4 ——字典
  9. 解决 python中 使用tesserocr,File tesserocr.pyx, line 2401, in tesserocr._tesserocr.image_to_text 报错问题...
  10. 单调栈 、 队列学习