Go错误处理规范化
参考
kuriball大佬公司内的文档
处理方式
目前在进行一个业务的错误处理优化,方便后续快速定位问题。
错误处理不当主要体现在以下方面:
- 没有打印堆栈信息,定位问题困难
- 重复携带堆栈信息,冗余
- 一个error打印多次(例如每一层都打印)
首先看几个和 日志 / error 相关的库
- errors标准库
- github.com/pkg/errors (下文直接称为pkg-errors)
- 公司内部 ecode(error code)
- 公司内部 log
堆栈信息是排查问题的一个重要手段,例如:
package main
import (
"fmt"
"github.com/pkg/errors"
)
func main() {
err := errors.New("whoops")
fmt.Printf("%+v", err)
// Example output:
// whoops
// github.com/pkg/errors_test.ExampleNew_printf
// /home/dfc/src/github.com/pkg/errors/example_test.go:17
// testing.runExample
// /home/dfc/go/src/testing/example.go:114
// testing.RunExamples
// /home/dfc/go/src/testing/example.go:38
// testing.(*M).Run
// /home/dfc/go/src/testing/testing.go:744
// main.main
// /github.com/pkg/errors/_test/_testmain.go:106
// runtime.main
// /home/dfc/go/src/runtime/proc.go:183
// runtime.goexit
// /home/dfc/go/src/runtime/asm_amd64.s:2059
}
需要注意格式化输出error时,%v只会输出错误文本,stack
根据堆栈信息我们可以快速定位到日志位置和调用链路
不携带堆栈信息方法:
- errors标准库 - errors.New
- pkg-errors - errors.WithMessage
- 公司内部的 ecode 创建error
携带堆栈信息的方法:
- pkg-errors 的 errors.New
- pkg-errors / errors - errors.Wrap
- pkg-errors - errors.WithStack
直接看源码即可,注释也很详细
公司内部在web框架中加入了中间件,会统一打印日志,推荐先把中间件的代码过一下,会更清楚整个流程
错误打印原则:
- 最底层(比如公共库)返回原始错误, 无需wrap
- 应用下层流转错误, 使用wrap带上堆栈
- 应用内方法互相调用, 避免重复wrap导致重复堆栈, WithMessage携带信息即可
公共库返回的error是否携带堆栈信息?
可以看下源码,例如json.Marshal
func Marshal(v any) ([]byte, error) {
e := newEncodeState()
defer encodeStatePool.Put(e)
err := e.marshal(v, encOpts{escapeHTML: true})
if err != nil {
return nil, err
}
buf := append([]byte(nil), e.Bytes()...)
return buf, nil
}
......
// jsonError is an error wrapper type for internal use only.
// Panics with errors are wrapped in jsonError so that the top-level recover
// can distinguish intentional panics from this package.
type jsonError struct{ error }
func (e *encodeState) marshal(v any, opts encOpts) (err error) {
defer func() {
if r := recover(); r != nil {
if je, ok := r.(jsonError); ok {
err = je.error
} else {
panic(r)
}
}
}()
e.reflectValue(reflect.ValueOf(v), opts)
return nil
}
先不去处理错误,发生panic后recover,从里面提取原因并转换为jsonError,一般发生panic后会程序崩溃会输出堆栈信息,但recover中只能获得报错信息,想要获取堆栈信息可以通过debug.Stack(),例如下面的代码:
package main
import (
"fmt"
"runtime/debug"
)
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("panic: %s\nstack trace:\n%s", r, debug.Stack())
}
}()
foo()
}
func foo() {
panic("oh foo")
}
/* output:
panic: oh foo
stack trace:
goroutine 1 [running]:
runtime/debug.Stack()
/usr/local/go-faketime/src/runtime/debug/stack.go:24 +0x65
main.main.func1()
/tmp/sandbox1024588529/prog.go:11 +0x39
panic({0x48a440, 0x4b83c8})
/usr/local/go-faketime/src/runtime/panic.go:884 +0x213
main.foo(...)
/tmp/sandbox1024588529/prog.go:18
main.main()
/tmp/sandbox1024588529/prog.go:14 +0x4a
*/
所以json.Marshal函数是不会输出堆栈信息,如果出现error了需要Wrap或者WithStack一下
wrap 或 WithMessage 后会不会影响到ecode对于前端的提示?
前端能够正确展示对应的提示是基于返回的ecode中的code和message, 前端展示message信息。
- wrap之前: 应用返回原始error(即为根因error), 如果该error为ecode, 拥有对应的code和message, 则前端能正常展示, 否则则不能
- wrap之后: 应用层通用返回原始error(即为根因error), wrap加上堆栈, 出口处框架会自动cause寻找根因, 之后和wrap之前逻辑一致
也就是说, 重点在于返回的原始error是否为ecode才是重点, 与是否wrap没有关系
例如:
// 方式一
if err != nil {
log.Errorc(c, "AddQASnapshot addQASnapshotDB error(%v) oid(%s)", err, req.Oid)
err = qaEcode.QASnapshotAddErr
}
// 方式二
if err != nil {
err = errors.WithMessagef(qaEcode.QASnapshotAddErr, "AddQASnapshot addQASnapshotDB oid(%s), error:%v", err, req.Oid)
}
两种方法都可以,方式一中,先将系统内的错误打印出来方便程序员问题定位,然后通过ecode替换为对应error,为用户展示错误信息。方式二是将qaEcode.QASnapshotAddErr作为原始error,将错误信息加到msg中,最后通过中间件统一打印。
举个例子更加直观一些:
package main
import (
"fmt"
"github.com/pkg/errors"
)
func main() {
err := errors.New("这是个err")
err2 := errors.New("这是个err2")
err3 := errors.WithMessagef(err, "err2:%+v", err2)
fmt.Printf("%+v\n", err3)
fmt.Println("errors.Cause:", errors.Cause(err3))
}
/* output:
这是个err
main.main
/tmp/sandbox4241311282/prog.go:9
runtime.main
/usr/local/go-faketime/src/runtime/proc.go:250
runtime.goexit
/usr/local/go-faketime/src/runtime/asm_amd64.s:1598
err2:这是个err2
main.main
/tmp/sandbox4241311282/prog.go:10
runtime.main
/usr/local/go-faketime/src/runtime/proc.go:250
runtime.goexit
/usr/local/go-faketime/src/runtime/asm_amd64.s:1598
errors.Cause: 这是个err
*/
评论