golang错误处理最佳实践
# golang错误处理最佳实践
本文讲述golang的错误处理的最佳实践。因为原始错误不带有堆栈信息,所以若想知道在哪一行出错,那么需要在每次出错的位置都打印一遍日志,这样带来的问题:相同的错误日志在不同层(如Hander、service、repository)重复打印。不仅增加了代码量,又增加了日志量,实在是没有必要。那么有没有好的解决办法呢?github.com/pkg/errors
就是为解决该类问题而生。本文围绕这个错误处理库,讲解错误处理的最佳实践。
首先需要明确2个概念:
- 原始错误
就是传统的使用golang errors包创建的错误,不会携带任何堆栈信息。
打印原始错误时,自然不会打印出堆栈信息,那么无法确定出错所在的文件行位置。 - 包装错误
可通过github.com/pkg/errors
包装原始错误,并支持额外附加上“上下文信息”、“堆栈信息”。
若想让原始错误带上堆栈信息,只需要使用github.com/pkg/errors
对原始
错误包装1
次即可。
注意,这里有几个重点
- 对原始错误做包装
对原始错误包装时,支持包装“上下文信息”和“当前堆栈”中的任何一个,也可以同时包装进去。- 对包装错误再次包装 对携带有堆栈的包装错误进行再次包装的时候,注意不要再使用WithStack等再次包装堆栈,否则打印的时候相同的堆栈信息会重复打印。
所以建议对“带有堆栈的包装错误”使用WithMessagef再次包装。
# 一. 常用的错误包装方法
github.com/pkg/errors
提供了如下的包装方法:
- Wrap
附加调用栈,支持附加上下文文本信息。
一般用于包装对第三方代码(标准库或第三方库)的调用。
注意:不要反复 Wrap ,会导致调用栈重复。
支持格式化字符串的版本:Wrapf - WithStack
附加调用栈, 但不支持附加上下文文本信息。 - WithMessage
若想在已有的错误(原始错误或堆栈错误)的基础上补充额外的上下文信息,不附加调用栈。
支持格式化字符串的版本:WithMessagef - Cause
用于反解出原始错误。
进而可以用来判断底层错误。 - errors.New
支持不依赖于已存在的原始错误,而直接创建1个带有堆栈信息的包装错误。
除了errors.New,还有errors.Errorf
等。
注意:
- 包装方法会基于
当前包装位置
生成堆栈信息。如A函数在位置AL处调用B函数,B函数在位置BL处调用C函数,C函数在位置CL处返回了错误,那么若B将C返回的错误进行了包装。那么该包装操作补充的堆栈信息包含“位置BL、位置AL”, 但并不会包含CL。 - 建议将当前函数的所有入参以及被调用函数的入参都包装到错误上下文中,以方便排查问题。
项目中推荐的用法:
- Repository层
使用errors.Wrapf返回包装错误 - Service层
对于Repository层返回的错误,使用errors.WithMessagef包装附加额外的上下文信息,并返回。
若要生成属于自己的自定义错误,那么使用errors.Wrapf包装错误, 并返回。 - Handler层
在Handler层接收到Service层返回的错误后,使用格式化字符串%+v
打印错误。
但是需要判断下错误的级别,若为系统错误【非程序上的错误】那么打印error级别的日志,否则打印warn级别的日志。
接口若返回错误,要么返回自定义错误,要么返回原始错误(通过errors.cause(err)方法来获取原始错误)。
注意使用
%+v
打印包装错误时,会按如下顺序打印错误信息:
1> 原始错误信息
2> 原始错误信息的附加上下文信息
3> 在原始错误上包装的堆栈
4> 在Service层包装的上下文信息
# 二. 创建堆栈错误
创建堆栈错误有2种方法:
- 使用包装方法包装1个原始错误
如:
import (
"testing"
"fmt"
stderr "errors"
"github.com/pkg/errors"
)
......
return errors.WithStack(stderr.New("has err"))
......
stderr.New
只是创建原始错误的其中一种方法,您可以根据实际需要更换,如fmt.Errorf
等。
因为原始错误和"github.com/pkg/errors"
错误的包名重名了,所以需要在import部分做下重命名。
- 直接创建1个堆栈错误
可以使用github.com/pkg/errors
包直接创建1个堆栈错误。
如:
import (
"testing"
"fmt"
stderr "errors"
"github.com/pkg/errors"
)
......
return errors.New("has err")
......
errors.New只是创建堆栈错误的其中一种方法,您可以根据实际需要更换,如errors.Errorf
等。
建议将当前函数的所有入参以及被调用函数的入参都包装到错误上下文中,以方便排查问题。
# 三. 打印错误
- 输出
1行
错误
使用 %v 作为格式化参数,那么错误信息会保持一行, 其中依次包含调用栈的上下文文本。 - 输出完整的调用栈
使用 %+v ,则会输出完整的调用栈详情。
# 四. 示例代码
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
...
*/
上次更新: 2022-03-19 00:49:36