最佳实践
panic
程序在启动时,如果有强依赖的服务出现故障时,需要panic退出
在程序启动时,如果发现有配置明显不符合要求,可以panic退出(防御编程)
其他情况下,只要不是不可恢复的程序错误,都不应该直接panic,而该返回error
在程序的入口处,例如gin需要使用recover中间件,来预防程序panic退出
-
在程序中我们应该避免使用野生的goroutine
i. 如果是在请求中需要执行大量的异步任务,应该使用异步worker,消息通知的方式进行处理,避免请求量大时大量goroutine被创建
ii. 如果需要使用goroutine时,应该使用统一的Go函数进行创建,这个函数中会对goroutine进行recover包装,避免因为野生goroutine报panic导致主协程奔溃退出
func Go(f func()){
go func(){
defer func(){
if err := recover(); err != nil {
log.Printf("panic: %+v", err)
}
}()
f()
}()
}
error
- 我们在应用程序中使用
github.com/pkg/errors
处理应用错误,注意在公共库中,我们一般不使用这个库 - error应该是函数的最后一个返回值,当error不为nil时,函数的其他返回值是不可用的状态,不应该对其他返回值做任何期待
i.func f() (io.Reader, *S1, error)
这个函数当error不为nil时,我们不知道 io.Reader 中是否有数据,可能有,也有可能有一部分 - 错误处理的时候应该先判断错误,
if 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
或fmt.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
添加堆栈信息:
i. 切记,不要每个地方都使用errors.Wrap
只需要在错误第一次出现时进行errors.Wrap
即可
ii. 根据场景进行判断是否需要将其他库的原始错误吞掉,例如可以把 repository 层的数据库相关错误吞掉,返回业务错误码,避免后续我们分割微服务或者更换 ORM 库时需要去修改上层代码
iii. 注意我们在基础库,被大量引入的第三方库编写时一般不使用 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
}
- 如何判定错误的信息是否足够,想一想当你的代码出现问题需要排查的时候你的错误信息是否可以帮助你快速的定位问题,例如我们在请求中一般会输出参数信息,用于辅助判断错误
- 对于业务错误,推荐在一个统一的地方创建一个错误字典,错误字典里面应该包含错误的 code,并且在日志中作为独立字段打印,方便做业务告警的判断,错误必须有清晰的错误文档
- 不需要返回并且被忽略的错误,必须输出日志信息
- 同一个地方不停的报错,最好不要不停输出错误日志,这样可能会导致被大量的错误日志信息淹没,无法排查问题,比较好的做法是打印一次错误详情,然后打印出错误出现的次数
- 对同一个类型的错误,采用相同的模式,例如参数错误,不要有的返回 404 有的返回 200
- 处理错误的时候,需要处理已分配的资源,使用 defer 进行清理,例如文件句柄