Go error 处理实践
文章转自: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")
即可,底层就是一个字符串结构体 errorString
s
当前 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 的问题有两点:
无法 wrap 更多的信息,比如调用栈,比如层层封装的 error 消息
无法很好的处理类型信息,比如我想知道错误是 io 类型的,还是 net 类型的
1.Wrap 更多的消息
这方面有很多轮子,最著名的就是 https://github.com/pkg/errors
, 我司也重度使用,主要功能有三个:
Wrap
封装底层 error, 增加更多消息,提供调用栈信息,这是原生 error 缺少的WithMessage
封装底层 error, 增加更多消息,但不提供调用栈信息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
, 原理非常简单,代码量非常少,感兴趣的可以看源码
这里一定要注意三点:
context
是谁传进来的?其它代码会不会用到,cancel 只能执行一次,瞎比用会出问题g.Go
不带 recover 的,为了程序的健壮,一定要自行 recover并行的 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 处理实践相关推荐
- Deep-Learning-YOLOV4实践:ScaledYOLOv4模型训练自己的数据集调试问题总结
error error1: CUDA out of memory error2:TypeError: can't convert cuda: error Deep-Learning-YOLOV4实践: ...
- Jetson部署实践
Jetson部署实践 一.模型开发 1.1 Yolov5图像检测模型开发 二.模型部署 2.1 DeepStream框架介绍 2.2 TensorRT 加速算子.模型转换 2.3 安装DeepStre ...
- 浅谈 GO 语言错误处理
go 的异常处理一直都是一种让人感觉奇怪的设计,本文用较多的篇幅和大家一起聊聊go 的异常处理的一些姿势 一.error 是什么玩意 话不多说 ,先放下源码(也就几行) package builtin ...
- 我爱机器学习网机器学习类别文章汇总
机器学习领域的几种主要学习方式 From Stumps to Trees to Forests KDD-2014 – The Biggest, Best, and Booming Data Scien ...
- 我爱机器学习--机器学习方向资料汇总
转载:http://blog.csdn.net/shuimanting520/article/details/45748505 机器学习爱好者资料 机器学习领域的几种主要学习方式 From Stump ...
- load xml error什么意思_XML文件解析实践(DOM解析)
昨天完成了基于DOM的XML文件解析类,今天赶紧实践了一下,不得不说,实践中的坑还是很多的. 本来这个项目就是为了规范各个服务在使用MySQL数据库时候的配置项,由于之前我接触的都是Java服务,对于 ...
- Go error 处理最佳实践
今天分享 go 语言 error 处理的最佳实践,了解当前 error 的缺点.妥协以及使用时注意事项.文章内容较长,干货也多,建议收藏 什么是 error 大家都知道 error[1] 是源代码内嵌 ...
- 〖ChatGPT实践指南 - 零基础扫盲篇⑥〗- OpenAI API 报错An error occurred during your request
帮助大家学习使用OpenAI的各类API开发应用 ,学习多个实站项目. 推荐他人订阅可获取扣除平台费用后的35%收益,文末有名片! 说明:该文属于 ChatGPT实践指南白宝书 专栏,购买任意白宝书体 ...
- ‘adb‘ 不是内部或外部命令和Error while executing: am start -n的解决实践
文章目录 前言 一.解决方法 1.引用参考,感谢前辈!! 2. 解决adb不是内部或外部命令 2.1.1. 点击该按钮 2.1.2. 打开上面文件夹 2.1.3. 验证是否可以 3. 解决Error ...
最新文章
- 吴明曦:马斯克的天基互联网与未来6G地基互联网优劣比较分析
- SqlServer数据库基础知识整理(不断更新~)
- java 之绘图技术
- geth bootnodes
- cpld xilinx 定义全局时钟_时钟相关概念
- 【SMTP 补录 Apache服务】
- SurvivalShooter学习笔记(八.敌人管理器)
- SQL分组求每组最大值问题的解决方法收集
- CI/CD(持续集成构建/持续交付):如何测试/集成/交付项目代码?(Jenkins,TravisCI)
- CentOS 7.X配置连接网络
- python股票交易模型_利用python建立股票量化交易系统(一)——小市值选股票模型...
- html 漂浮 广告置顶,jquery浮动图片广告代码_页面上漂浮图片广告代码
- [na]win7系统安装在t450s
- Delphi程序破解技术概要
- 一张“黑洞”照片需半吨重硬盘?更逆天的操作还有这些……
- PCF应用管理平台介绍(PCF Apps Manager)
- linux开机启动出现grup,开机出现grub解决方法
- Hexo个人博客搭建教程
- 【转】Thunderbird on Ubuntu 12.04 – 调整邮件列表行间距
- java设计模式——门面与调停
热门文章
- java高效获取大文件的行数
- iPhone 诈骗又出新招,别看见弹窗就输密码
- 作业6--四则运算APP之Sprint计划
- 基于jetty9 编程构建嵌入式https 服务器
- C#编程指南:使用属性
- 在路由器与交换机之间添加ISA Server软路由与防火墙
- 应用程序架构指导袖珍版
- Python 基础学习 4 ——字典
- 解决 python中 使用tesserocr,File tesserocr.pyx, line 2401, in tesserocr._tesserocr.image_to_text 报错问题...
- 单调栈 、 队列学习