Golang中的error
Golang中的 error 就是一个简单的接口类型。只要实现了这个接口,就可以将其视为一种 error
type error interface { | |
Error() string | |
} |
error的几种玩法
翻看Golang源码,能看到许多类似于下面的这两种error类型
哨兵错误
var EOF = errors.New("EOF") | |
var ErrUnexpectedEOF = errors.New("unexpected EOF") | |
var ErrNoProgress = errors.New("multiple Read calls return no data or error") |
缺点:
1.让 error 具有二义性
error != nil不再意味着一定发生了错误
比如io.Reader返回io.EOF来告知调用者没有更多数据了,然而这又不是一个错误
2.在两个包之间创建了依赖
如果你使用了io.EOF来检查是否read完所有的数据,那么代码里一定会导入io包
自定义错误类型
一个不错的例子是os.PathError,它的优点是可以附带更多的上下文信息
type PathError struct { | |
Op string | |
Path string | |
Err error | |
} |
Wrap error
到这里我们可以发现,Golang 的 error 非常简单,然而简单也意味着有时候是不够用的
Golang的error一直有两个问题:
1.error没有附带file:line信息(也就是没有堆栈信息)
比如这种error,鬼知道代码哪一行报了错,Debug时简直要命
SERVICE ERROR 2022-03-25T16:32:10.687+0800!!! | |
Error 1406: Data too long for column 'content' at row 1 |
2.上层error想附带更多日志信息时,往往会使用fmt.Errorf()
,fmt.Errorf()
会创建一个新的error,底层的error类型就被“吞”掉了
var errNoRows = errors.New("no rows") | |
// 模仿sql库返回一个errNoRows | |
func sqlExec() error { | |
return errNoRows | |
} | |
func serviceNoErrWrap() error { | |
err := sqlExec() | |
if err != nil { | |
return fmt.Errorf("sqlExec failed.Err:%v", err) | |
} | |
return nil | |
} | |
func TestErrWrap(t *testing.T) { | |
// 使用fmt.Errorf创建了一个新的err,丢失了底层err | |
err := serviceNoErrWrap() | |
if err != errNoRows { | |
log.Println("===== errType don't equal errNoRows =====") | |
} | |
} | |
-------------------------------代码运行结果---------------------------------- | |
=== RUN TestErrWrap | |
2022/03/26 17:19:43 ===== errType don't equal errNoRows ===== |
为了解决这个问题,我们可以使用github.com/pkg/error包
,使用errors.withStack()方法
将err保
存到withStack对象
// withStack结构体保存了error,形成了一条error链。同时*stack字段保存了堆栈信息。 | |
type withStack struct { | |
error | |
*stack | |
} |
也可以使用errors.Wrap(err, "自定义文本")
,额外附带一些自定义的文本信息
源码解读:先将err和message包进withMessage对象
,再将withMessage对象
和堆栈信息包进withStack对象
func Wrap(err error, message string) error { | |
if err == nil { | |
return nil | |
} | |
err = &withMessage{ | |
cause: err, | |
msg: message, | |
} | |
return &withStack{ | |
err, | |
callers(), | |
} | |
} |
Golang1.13版本error的新特性
Golang1.13版本借鉴了github.com/pkg/error包
,新增了如下函数,大大增强了 Golang 语言判断 error 类型的能力
errors.UnWrap()
// 与errors.Wrap()行为相反 | |
// 获取err链中的底层err | |
func Unwrap(err error) error { | |
u, ok := err.(interface { | |
Unwrap() error | |
}) | |
if !ok { | |
return nil | |
} | |
return u.Unwrap() | |
} |
errors.Is()
在1.13版本之前,我们可以用err == targetErr
判断err类型
errors.Is()
是其增强版:error 链上的任一err == targetErr
,即return true
// 实践:学习使用errors.Is() | |
var errNoRows = errors.New("no rows") | |
// 模仿sql库返回一个errNoRows | |
func sqlExec() error { | |
return errNoRows | |
} | |
func service() error { | |
err := sqlExec() | |
if err != nil { | |
return errors.WithStack(err) // 包装errNoRows | |
} | |
return nil | |
} | |
func TestErrIs(t *testing.T) { | |
err := service() | |
// errors.Is递归调用errors.UnWrap,命中err链上的任意err即返回true | |
if errors.Is(err, errNoRows) { | |
log.Println("===== errors.Is() succeeded =====") | |
} | |
//err经errors.WithStack包装,不能通过 == 判断err类型 | |
if err == errNoRows { | |
log.Println("err == errNoRows") | |
} | |
} | |
-------------------------------代码运行结果---------------------------------- | |
=== RUN TestErrIs | |
2022/03/25 18:35:00 ===== errors.Is() succeeded ===== |
例子解读:
因为使用errors.WithStack
包装了sqlError
,sqlError
位于error链的底层,上层的error已经不再是sqlError
类型,所以使用==
无法判断出底层的sqlError
源码解读:
- 我们很容易想到其内部调用了
err = Unwrap(err)
方法来获取error链中底层的error - 自定义error类型可以实现
Is接口
来自定义error类型判断方法
func Is(err, target error) bool { | |
if target == nil { | |
return err == target | |
} | |
isComparable := reflectlite.TypeOf(target).Comparable() | |
for { | |
if isComparable && err == target { | |
return true | |
} | |
// 支持自定义error类型判断 | |
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) { | |
return true | |
} | |
if err = Unwrap(err); err == nil { | |
return false | |
} | |
} | |
} |
下面我们来看看如何自定义error类型判断:
自定义的errNoRows类型
,必须实现Is接口
,才能使用erros.Is()
进行类型判断
type errNoRows struct { | |
Desc string | |
} | |
func (e errNoRows) Unwrap() error { return e } | |
func (e errNoRows) Error() string { return e.Desc } | |
func (e errNoRows) Is(err error) bool { | |
return reflect.TypeOf(err).Name() == reflect.TypeOf(e).Name() | |
} | |
// 模仿sql库返回一个errNoRows | |
func sqlExec() error { | |
return &errNoRows{"Kaolengmian NB"} | |
} | |
func service() error { | |
err := sqlExec() | |
if err != nil { | |
return errors.WithStack(err) | |
} | |
return nil | |
} | |
func serviceNoErrWrap() error { | |
err := sqlExec() | |
if err != nil { | |
return fmt.Errorf("sqlExec failed.Err:%v", err) | |
} | |
return nil | |
} | |
func TestErrIs(t *testing.T) { | |
err := service() | |
if errors.Is(err, errNoRows{}) { | |
log.Println("===== errors.Is() succeeded =====") | |
} | |
} | |
-------------------------------代码运行结果---------------------------------- | |
=== RUN TestErrIs | |
2022/03/25 18:35:00 ===== errors.Is() succeeded ===== |
errors.As()
在1.13版本之前,我们可以用if _,ok := err.(targetErr)
判断err类型
errors.As()
是其增强版:error 链上的任一err与targetErr类型相同
,即return true
// 通过例子学习使用errors.As() | |
type sqlError struct { | |
error | |
} | |
func (e *sqlError) IsNoRows() bool { | |
t, ok := e.error.(ErrNoRows) | |
return ok && t.IsNoRows() | |
} | |
type ErrNoRows interface { | |
IsNoRows() bool | |
} | |
// 返回一个sqlError | |
func sqlExec() error { | |
return sqlError{} | |
} | |
// errors.WithStack包装sqlError | |
func service() error { | |
err := sqlExec() | |
if err != nil { | |
return errors.WithStack(err) | |
} | |
return nil | |
} | |
func TestErrAs(t *testing.T) { | |
err := service() | |
// 递归使用errors.UnWrap,只要Err链上有一种Err满足类型断言,即返回true | |
sr := &sqlError{} | |
if errors.As(err, sr) { | |
log.Println("===== errors.As() succeeded =====") | |
} | |
// 经errors.WithStack包装后,不能通过类型断言将当前Err转换成底层Err | |
if _, ok := err.(sqlError); ok { | |
log.Println("===== type assert succeeded =====") | |
} | |
} | |
----------------------------------代码运行结果-------------------------------------------- | |
=== RUN TestErrAs | |
2022/03/25 18:09:02 ===== errors.As() succeeded ===== |
例子解读:
因为使用errors.WithStack
包装了sqlError
,sqlError
位于error链的底层,上层的error已经不再是sqlError
类型,所以使用类型断言无法判断出底层的sqlError
error处理最佳实践
上面讲了如何定义error类型,如何比较error类型,现在我们谈谈如何在大型项目中做好error处理
优先处理error
当一个函数返回一个非空error时,应该优先处理error,忽略它的其他返回值
只处理error一次
在Golang中,对于每个err,我们应该只处理一次。
- 要么立即处理err(包括记日志等行为),return nil(把错误吞掉)。此时因为把错误做了降级,一定要小心处理函数返回值。
比如下面例子json.Marshal(conf)没有return err ,那么在使用buf时一定要小心空指针等错误
- 要么return err,在上层处理err
反例:
// 试想如果writeAll函数出错,会打印两遍日志 | |
// 如果整个项目都这么做,最后会惊奇的发现我们在处处打日志,项目中存在大量没有价值的垃圾日志 | |
// unable to write:io.EOF | |
// could not write config:io.EOF | |
type config struct {} | |
func writeAll(w io.Writer, buf []byte) error { | |
_, err := w.Write(buf) | |
if err != nil { | |
log.Println("unable to write:", err) | |
return err | |
} | |
return nil | |
} | |
func writeConfig(w io.Writer, conf *config) error { | |
buf, err := json.Marshal(conf) | |
if err != nil { | |
log.Printf("could not marshal config:%v", err) | |
} | |
if err := writeAll(w, buf); err != nil { | |
log.Println("count not write config: %v", err) | |
return err | |
} | |
return nil | |
} |
不要反复包装error
我们应该包装error,但只包装一次
上层业务代码建议Wrap error
,但是底层基础Kit库不建议
如果底层基础 Kit 库包装了一次,上层业务代码又包装了一次,就重复包装了 error,日志就会打重
比如我们常用的sql库
会返回sql.ErrNoRows
这种预定义错误,而不是给我们一个包装过的 error
不透明的错误处理
在大型项目中,推荐使用不透明的错误处理(Opaque errors)
:不关心错误类型,只关心error是否为nil
好处:
- 耦合小,不需要判断特定错误类型,就不需要导入相关包的依赖。
-
- 不过有时候,这种处理error的方式不够用,比如:业务需要对
参数异常error类型
做降级处理,打印Warn级别的日志
type ParamInvalidError struct { | |
Desc string | |
} | |
func (e ParamInvalidError) Unwrap() error { return e } | |
func (e ParamInvalidError) Error() string { return "ParamInvalidError: " + e.Desc } | |
func (e ParamInvalidError) Is(err error) bool { | |
return reflect.TypeOf(err).Name() == reflect.TypeOf(e).Name() | |
} | |
func NewParamInvalidErr(desc string) error { | |
return errors.WithStack(&ParamInvalidError{Desc: desc}) | |
} | |
------------------------------顶层打印日志--------------------------------- | |
if errors.Is(err, Err.ParamInvalidError{}) { | |
logger.Warnf(ctx, "%s", err.Error()) | |
return | |
} | |
if err != nil { | |
logger.Errorf(ctx, " error:%+v", err) | |
} |
简化错误处理
Golang因为代码中无数的if err != nil
被诟病,现在我们看看如何减少if err != nil
这种代码
bufio.scan
CountLines() 实现了”读取内容的行数”功能
可以利用 bufio.scan() 简化 error 的处理:
func CountLines(r io.Reader) (int, error) { | |
var ( | |
br = bufio.NewReader(r) | |
lines int | |
err error | |
) | |
for { | |
_, err := br.ReadString('\n') | |
lines++ | |
if err != nil { | |
break | |
} | |
} | |
if err != io.EOF { | |
return 0, nilsadwawa | |
} | |
return lines, nil | |
} | |
func CountLinesGracefulErr(r io.Reader) (int, error) { | |
sc := bufio.NewScanner(r) | |
lines := 0 | |
for sc.Scan() { | |
lines++ | |
} | |
return lines, sc.Err() | |
} |
bufio.NewScanner()
返回一个 Scanner
对象,结构体内部包含了 error 类型,调用Err()
方法即可返回封装好的error
Golang源代码中蕴含着大量的优秀设计思想,我们在阅读源码时从中学习,并在实践中得以运用
type Scanner struct { | |
r io.Reader // The reader provided by the client. | |
split SplitFunc // The function to split the tokens. | |
maxTokenSize int // Maximum size of a token; modified by tests. | |
token []byte // Last token returned by split. | |
buf []byte // Buffer used as argument to split. | |
start int // First non-processed byte in buf. | |
end int // End of data in buf. | |
err error // Sticky error. | |
empties int // Count of successive empty tokens. | |
scanCalled bool // Scan has been called; buffer is in use. | |
done bool // Scan has finished. | |
} | |
func (s *Scanner) Err() error { | |
if s.err == io.EOF { | |
return nil | |
} | |
return s.err | |
} |
errWriter
WriteResponse()
函数实现了"构建HttpResponse"
功能
利用上面学到的思路,我们可以自己实现一个errWriter
对象,简化对 error 的处理
type Header struct { | |
Key, Value string | |
} | |
type Status struct { | |
Code int | |
Reason string | |
} | |
func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { | |
_, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) | |
if err != nil { | |
return err | |
} | |
for _, h := range headers { | |
_, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value) | |
if err != nil { | |
return err | |
} | |
} | |
if _, err := fmt.Fprintf(w, "\r\n"); err != nil { | |
return err | |
} | |
_, err = io.Copy(w, body) | |
return err | |
} | |
type errWriter struct { | |
io.Writer | |
err error | |
} | |
func (e *errWriter) Write(buf []byte) (n int, err error) { | |
if e.err != nil { | |
return 0, e.err | |
} | |
n, e.err = e.Writer.Write(buf) | |
return n, nil | |
} | |
func WriteResponseGracefulErr(w io.Writer, st Status, headers []Header, body io.Reader) error { | |
ew := &errWriter{w, nil} | |
fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) | |
for _, h := range headers { | |
fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value) | |
} | |
fmt.Fprintf(w, "\r\n") | |
io.Copy(ew, body) | |
return ew.err | |
} |
何时该用panic
在 Golang 中panic
会导致程序直接退出,是一个致命的错误。
建议发生致命的程序错误时才使用 panic,例如索引越界、不可恢复的环境问题、栈溢出等等
小补充
errors.New()
返回的是errorString对象
的指针,其原因是防止字符串产生碰撞,如果发生碰撞,两个 error 对象会相等。
源码:
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 | |
} |
实践:error1
和error2
的text都是"error"
,但是二者并不相等
func TestErrString(t *testing.T) { | |
var error1 = errors.New("error") | |
var error2 = errors.New("error") | |
if error1 != error2 { | |
log.Println("error1 != error2") | |
} | |
} | |
---------------------代码运行结果-------------------------- | |
=== RUN TestXXXX | |
2022/03/25 22:05:40 error1 != error2 |
创作不易,希望大家能顺手点个赞~这对我很重要,蟹蟹各位啦~
参考文献
《Effective GO》
《Go程序设计语言》
dave.cheney.net/practical-go/prese...