应价交易系统网站开发,微信h5制作平台,常德网站建设全域云,源码如何搭建网站今天分享 go 语言 error 处理的最佳实践#xff0c;了解当前 error 的缺点、妥协以及使用时注意事项。文章内容较长#xff0c;干货也多#xff0c;建议收藏
什么是 error
大家都知道 error[1] 是源代码内嵌的接口类型。根据导出原则#xff0c;只有大写的才能被其它源码…今天分享 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(fUnexpected {err}, {type(err)})raise比如上面是 python try-catch 的用法先写一堆逻辑不处理异常最后统一捕获
let mut cfg self.check_and_copy()?;相比来说 rust Result 模式更简洁一个 ? 就代替了我们的操作。但是 error 的繁琐判断是当前的痛点嘛显然不是尤其喜欢 c 语言的人反而喜欢每次都做判断
在我看来 go 的痛点不是缺少泛型不是 error 太挫而是 GC 太弱尤其对大内存非常不友好
当前 error 的问题有两点 无法 wrap 更多的信息比如调用栈比如层层封装的 error 消息 无法很好的处理类型信息比如我想知道错误是 io 类型的还是 net 类型的
1.Wrap 更多的消息
这方面有很多轮子最著名的就是 https://github.com/pkg/errors, 我司也重度使用主要功能有三个
Wrap 封装底层 error, 增加更多消息提供调用栈信息这是原生 error 缺少的
WithMessage 封装底层 error, 增加更多消息但不提供调用栈信息
Cause 返回最底层的 error, 剥去层层的 wrap
import (database/sqlfmtgithub.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/sqlfmt
)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/sqlerrorsfmt
)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 文件解析失败端口监听失败等等所有业务逻辑禁止主动 panicpanic如果没有recover会导致整个程序中断
根据 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 fmttype 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 (fmtgithub.com/myteksi/hystrix-go/hystrixtime
)
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,