error or panic
panic
- 程序启动时,如果有强依赖服务出故障可以panic
- 程序启动时,配置出错,可以panic
- 其他情况都不允许该panic,应该返回error
- 在入口处,gin的中间件有recovery预防panic
- 在程序中避免使用野生goroutine
- 如果请求需要执行异步任务,应该使用异步的worker,比如消息通知的方式,避免请求一次创建一个goroutine(虽然net也是这样)
- 如果必须创建goroutine,那么应该使用一个func统一创建处理,避免野生goroutine panic导致主进程退出
func Go(f func()) {
go func() {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %+v", err)
}
}()
}()
}
error
-
我们在应用程序中使用 github.com/pkg/errors 处理应用错误,注意在公共库当中,我们一般不使用这个
-
error应该是函数的最后一个返回值,当error != nil 的时候,函数不该对其他值有不明确的返回,必须是不可用
- func f() (io.Reader, *S1, error) , 这个函数就没法对io.Reader做出明确定义
-
错误处理首先判断错误,出现err != nil 立即返回,避免嵌套处理
// good case
func f() error {
a, err := A()
if err != nil {
return err
}
// ... 其他逻辑
return nil
}
// bad case
func f() error {
a, err := A()
if err == nil {
// 其他逻辑
}
return err
}
- 应用程序出现错误时,使用errors.New活着errors.Errorf返回错误
func (u *usecese) usecase1() error {
money := u.repo.getMoney(uid)
if money < 10 {
errors.Errorf("用户余额不足, uid: %d, money: %d", uid, money)
}
// 其他逻辑
return nil
}
- 如果是应用程序的其他函数出错,应该立即返回,如果需要写带信息,使用errors.WithMessage
func (u *usecese) usecase2() error {
name, err := u.repo.getUserName(uid)
if err != nil {
return errors.WithMessage(err, "其他附加信息")
}
// 其他逻辑
return nil
}
- 如果是调用其他库(标准库,公共库,第三方...)出现错误,请使用 errors.Wrap 添加堆栈信息
- 不需要每个地方都wrap,只需要在第一次出现的地方进行一次wrap即可
- 根据场景进行判断是否需要将其他库的原始错误吞掉,例如可以把 repository 层的数据库相关错误吞掉,返回业务错误码,避免后续我们分割微服务或者更换 ORM 库时需要去修改上层代码
- 注意我们在基础库,被大量引入的第三方库编写时一般不使用 errors.Wrap 避免堆栈信息重复,因为第三方可能也有相关处理
func f() error {
err := json.Unmashal(&a, data)
if err != nil {
return errors.Wrap(err, "其他附加信息")
}
// 其他逻辑
return nil
}
- 禁止每个出错地方都打日志,只需要在进程最开始的地方使用%+v,例如http/rpc服务的中间件
- 错误判断使用errors.Is比较
func f() error {
err := A()
if errors.Is(err, io.EOF){
return nil
}
// 其他逻辑
return nil
}
- 错误类型判断,使用errors.As
func f() error {
err := A()
var errA errorA
if errors.As(err, &errA){
// ...
}
// 其他逻辑
return nil
}
- 如何判断错误信息足够?想一下当出现错误排查时候是否信息足够,比如请求就需要一些输出参数的信息
- 对于业务错误,推荐在一个统一地方创建一个错误字典,字典里应该包括错误码(error code),并且在日志中独立打出来,同时要有清晰的错误文档
- 不需要返回,忽略的错误必须输出到日志信息中
- 同一个地方不停的报错,尽量避免不停输出日志,可以打印一次日志详情,然后打印错误出现的次数,避免被大量的日志信息淹没别的信息
- 对于统一类型的错误,采用相同的模式,比如参数错误,不要一个错误码是400,另一个是401
- 处理错误时,需要主要是否需要处理分配资源,使用defer,或者尽量不适用defer手动是否句柄
- goroutine 用%+v 打印堆栈详情
panic or error?
-
在Go中panic会导致程序直接退出,如果使用panic recovery性能会受影响
- 性能问题
- 容易漏掉panic处理,导致主进程意外退出
- 不可控, 错误交给了外部
-
什么时候使用panic
- 对于真正的崩溃情况, 比如索引越界,环境问题,栈溢出,大致就是不可恢复的错误
-
使用error好处
- 简单
- plan for failure not success, 代码逻辑考虑错误情况的处理
- 没有隐藏
- 自主可控的错误
- 错误也是值
error handle
// 统计文件行数
func count(r io.Reader) (int, error) {
var (
br = bufio.NewReader(r)
lines int
err error
)
for {
// 读取到换行符就说明是一行
_, err = br.ReadString('\n')
lines++
if err != nil {
break
}
}
// 当错误是 EOF 的时候说明文件读取完毕了
if err != io.EOF {
return 0, err
}
return lines, err
}
func count2(r io.Reader) (int, error) {
var (
sc = bufio.NewScanner(r)
lines int
)
for sc.Scan() {
lines++
}
return lines, sc.Err()
}
error writer
_, 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
err writer
type errWriter struct {
w io.Writer
err 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
}
为什么不需要处处使用errors.Wrap
因为每一次 errors.Wrap 的调用都会为错误添加堆栈信息,如果处处调用那会有大量的无用堆栈
func main() {
fmt.Printf("err: %+v", c())
}
func a() error {
return errors.Wrap(fmt.Errorf("xxx"), "test")
}
func b() error {
return a()
}
func c() error {
return b()
}
结果是已经打印出全部的堆栈信息了
err: xxx
test
main.a
/home/ll/project/Go-000/Week02/blog/wrap.go:14
main.b
/home/ll/project/Go-000/Week02/blog/wrap.go:18
main.c
/home/ll/project/Go-000/Week02/blog/wrap.go:22
main.main
/home/ll/project/Go-000/Week02/blog/wrap.go:10
runtime.main
/usr/local/go/src/runtime/proc.go:204
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1374
一个没有高级趣味的人。
email:hushui502@gmail.com