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

ref
http://lailin.xyz/post/go-training-03.html

posted @ 2020-12-14 15:36  zhangyu63  阅读(176)  评论(0编辑  收藏  举报