背景
在 C#、Java 等语言中常常使用 try...catch的方式来捕获异常。但是在Golang 对于错误处理有不同的方式。像网上也有很多对 error 处理的最佳实践的文章,其中很多其实就是对 error 的统一封装,使用规范进行约束。本文主要是记录自己对处理 Error 的一些认识和学习。
error 基本概念
go
type error interface{
Error() string
}
Golang 中 error类型是一个具有单个方法的接口,其内置方法是返回描述错误的字符串。Go 语言创始人觉得异常机制会导致控制流程混乱,降低代码的可读性,所以期望异常是一种可预知的。所以在使用时应当适当的返回 error ,然后调用方就可以更明确知道错误,进而再处理异常。同样也说明了开发人员在 coding 时要能识别和预测可能发生错误,再进行处理,而不是忽略错误,
panic 和 recover
但可能也会出现一些我们无法预料的错误情况。比较严重的程序错误导致发生 panic 就会使程序结束,所以它又提供一个 recover 来捕获 panic ,以便发生panic时能够重新执行启动程序。
go
defer func() {
if r := recover(); r != nil {
fmt.Println("重新启动程序:", r)
}
}()
recover 函数最好在主函数中且必须在 defer 语句总使用,为了保证发生 panic 时被调用,如果没有panic时recover 函数将会返回 nil。
对于panic 的错误,应当全面记录错误信息,这样才有利于排查问题。对于一些比较重要的服务应当适当增加报警机制以通知相关团队或人员。
创建错误
go
// 方式1:使用 fmt.Errorof("")
func div(x, y int)(int,error){
if y==0 {
return 0,fmt.Errorof("除数 %d 不能为0",y)
}
return x/y, nil
}
// 方式2: 使用errors.New("")
ErrDivByZero := errors.New("除数不能为0")
return 0,ErrDivByZero
// 方式3:使用 errors.Wrap(err, "除数不能为0")
return 0,errors.Wrap(err, "除数不能为0")
方式1中 fmt.Errorf 函数允许使用格式化字符串创建新错误。
errors.Wrap
函数允许使用上下文包装error,使用场景:当函数调用另一个方法时遇到的错误而导致无法完成业务流程,那可能需要从函数返回错误。使用 errors.Wrap 函数需要注意:因其除了错误信息外还附加了堆栈信息,所以不能在程序中大量使用。
以上3种可以很简单方便的创建错误。当然支持自定义错误类型,通过创建实现 error 接口的新类型。例如:
go
// 方式4: 自定义错误类型
type MathCalError struct {
message string
}
// 实现 Error 方法
func (e *MathCalError) Error() string {
return e.message
}
func div(x, y int) (int, error) {
if y == 0 {
return 0, &MathCalError{"除数不能为 0"}
}
return x / y, nil
}
处理错误
根据遇到到场景,基本对于错误的处理大致有下面五种情况:
- 忽略错误:调用方对于发生错误觉得没有任何影响,那么可以不接收直接忽略
- 记录错误并继续执行:调用方觉得错误无影响但需要进行记录,可以接受记录
- 传递错误:继续传递错误。
- 错误重试:某功能错误需要再重试,如请求第三方接口超时时想再重试。设置重试次数限制,以防止错误持续存在时出现无限循环。
- 严重错误终止程序:若有些错误期望终止程序,那直接 panic 处理。
go
// 忽略错误
result,_ := getUser()
// 记录错误
result,err := getUser();err!=nil{
log,writerInfo("something error", err.Error())
}
// 传递错误
result,err := getUser();err!=nil{
return err
}
// 错误重试
retryCount := 0
maxRetryCount := 3
for {
result,err := getUser();
if err!=nil{
return err
if retryCount >= maxRetryCount{
return err
}
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Duration(retryCount) * time.Second):
retryCount++
continue
}
}
//错误后直接 panic
if err := getUser(); err != nil {
panic("something wrong")
}
项目中使用 Error 的方案
方案一 :定义 ErrorMessage 包
项目中可以定义一个含有所有错误信息的包,然后在使用时直接引用。
go
package errorMsg
import "errors"
var UndefinedError = errors.New("未定义")
var InvalidateParamsError = errors.New("不合法参数")
// main 函数中使用
func getUser() error {
return errorMsg.UndefinedError
}
方案二:自定义 error 类型
go
type ParamNotFound NotFound
type ValueNotFound NotFound
type NotFound struct {
msg string
}
func (n *NotFound) Error() string {
return n.msg
}
// 通过使用 switch 处理不同类型的错误,
func handleErrors(err error) {
switch v := err.(type) {
case ParamNotFound:
fmt.Printf("ParamNotFound: %v\n", v)
case ValueNotFound:
fmt.Printf("ValueNotFound: %v\n", v)
default:
fmt.Printf("Other error: %v\n", v)
}
}
最后
有效的错误处理对于构建可靠的服务至关重要,个人觉得最佳的实践是团队内部的统一规范。没有最佳实践和方案,只有最适合的你的场景的实践。