Go语言错误处理方式
错误定义方式
1. Sentinel errors
在很多包中甚至标准库中都会见到以errors.New() 方法获取一个错误并把它赋值为一个包级别的常量,如io.EOF , 这种特定的错误是API在某些场景下需要返回一个特定的错误interface 即使有更具描述性的错误可以返回。
这样做会使包之间的源代码产生依赖关系,如检查错误是否是io.EOF, 项目必须导入io包。这样做很常见但是如果你的项目中很多包都要导出错误值,那么其他包必须依赖这些错误值才能做特定的错误检查,代码极度耦合。
所以尽量避免出现这样错误处理
2. Error types
把error处理成自定义的Type,这样做可以在error中添加一些上下文信息,以及更多的描述性信息。
1 | type MyError struct{ |
1 | switch err := err.(type){ |
缺点:这种模型必须要暴露自定义的error类型供调用者调用,使得和调用者产生强耦合,使得API变得脆弱。调用者也必须要导入错误包。建议避免使用错误类型,至少便面把它作为公共API的一部分。
3. Error Interface
非透明的错误传递不会强依赖于包内变量,发现错误就向外抛出是最好的处理错误方式,但是如果外界要对包内错误进行判断就十分繁琐,不建议使用contain方法去强行匹配错误某些字段,这样很容易出现难以发现的bug。如何解决这一问题,我们可以定义一个行为而不是一个方式如:
1 | type certErr interface{ |
这样不用暴露包内的错误类型与错误变量,可以直接通过方法来判断某个错误。减少强依赖。
错误处理方式
1. 错误直接抛出
不要使用err==nil 这样很容易使得代码无线缩进,建议在错误出现时直接抛出
2. 减少错误处理
使用多次调用某方法可以使用一个结构将err包裹,出错存储。这样在调用该方法时不用做错误处理
1 | type errWriter struct{ |
以上处理方式在bufio.Scan()中也有类似操作
3. 使用 pkg error 包
我们的代码经常会遇到错误一层层向上抛出,但是因为缺乏上下文描述信息导致错误无法追踪,处理这个问题要么是错误日志四处打印,要么是痛苦的debug。此时巧用pkg error包可以消除痛点。
这个库的灵魂是wrap, 最烦的也是wrap。
在应用代码中的自定义错误如长度过长,不符合正则,使用errors.new 或者 errors.Errorf 返回错误
如果调用项目中其他包内函数直接返回err
调用自己的基础库,第三方库,官方基础库使用errors.Wrap 或 errors.Wrapf
直接返回错误,不要打日志
在程序的顶部或是工作的goroutine的顶部(请求入口),使用%+v把堆栈详情记录下来
使用errors.Cause 获取root error 再和依赖库的sentienal error 去判定
如果是基础库(被很多项目使用的依赖第三方库,返回根错误),不要去进行wrap 只在应用中使用
4. 使用 go 1.3 的errors 包
- go 1.3 为errors 和 fmt 标准库添加了新的的使用方法,从而简化处理包含其他错误的错误。其中error 实现了一个 Unwrap 的方法,如果err1 包含 err2 使用Unwrap(err1) 返回err2
1 | func (e *QueryError) Unwrap()error {return e.Err} |
- go 1.3 还包含两个用于检查错误的新函数,Is ,As
1 | // An error is considered to match a target if it is equal to that target or if |
其实底层是不断地调用unwrap一旦错误相同就返回true。
1 | // As will panic if target is not a non-nil pointer to either a type that implements |
也是不断Unwrap ,一旦错误匹配就返回true 并把错误解析进相应的格式中。
- go 1.3 支持新的%w ,使用%w 的错误站位符可以把原始Error包入,支持Is, As
1 | err := fmt.Errorf("access not support : %w",ErrAuth) |
example 1
1 |
|
example 2
如果直接返回,会导致调用者依赖自定义错误,如果在ErrPermission中带了自定义字段,那么就无法判断,使用%w,则可加入字段,使用Is方法来判定
1 | var ErrPermission = errors.New("permission denied") |
5. go 1.3 结合 pkg error 使用
总结
- 错误判定不要强依赖。
- 错误日志只在顶层打印,不要四处打印
- 错误的堆栈信息只在错误第一次出现时存储,如第三方基础库调用时,sql语句出问题时。
- 尽量减少错误处理
根据这几点举个错误处理例子:
1 | import( |
异常捕
go 语言的服务框架一般都自带recover 但在起go程并发时,野生go程的panic是无法捕获的,这经常会造成程序崩溃,推荐以下方式去recover
1 | func Go(x func()){ |