最佳实践之Golang错误处理
1、原生错误处理
Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
error类型是一个接口类型,这是它的定义:
type error interface {
Error() string
}
?
我们可以在编码中通过实现 error 接口类型来生成错误信息。
函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// 实现
}
在下面的例子中,我们在调用Sqrt的时候传递的一个负数,然后就得到了non-nil的error对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误,请看下面调用的示例代码:
result, err:= Sqrt(-1)
if err != nil {
fmt.Println(err)
}
2、开源error包 github.com/pkg/errors包在原生error包基础上增加了以下常用的功能:
- 可以打印error的堆栈信息:打印错误需要%+v才能详细输出
- 使用Wrap或Wrapf,初始化一个error
- 使用errors.WithMessage可以在原来的error基础上再包装一层,包含原有error信息
- errors.Is,用于判断error类型,可根据error类型不同做不同处理
- errors.As,用于解析error
3、工程中错误处理 3.1 需求整理
- 自定义error信息,并进行编码整理
- controller层可以判断自定义error类型,最终判断是按info处理,还是按error处理
- 可以打印error初始发生的位置(获取error的调用栈)
- 确认当前系统定位:
- 用户,获取TagMessage
- 上游服务,需要错误码映射
- 日志监控、监控TagMessage
github.com/pkg/errors
包,完整实现一套的错误处理机制3.2 方式一:Map保存错误码与Message的映射 3.2.1 定义错误信息
新建error_handler.go
package error_handleimport (
"github.com/pkg/errors"
)// 1、自定义error结构体,并重写Error()方法
// 错误时返回自定义结构
type CustomError struct {
Codeint`json:"code"`// 业务码
TagMessage string `json:"message"` // 描述信息
}func (e *CustomError) Error() string {
return e.TagMessage
}// 2、定义errorCode
const (
// 服务级错误码
ServerError= 10101
TooManyRequests= 10102
ParamBindError= 10103
AuthorizationError = 10104
CallHTTPError= 10105
ResubmitError= 10106
ResubmitMsg= 10107
HashIdsDecodeError = 10108
SignatureError= 10109// 业务模块级错误码
// 用户模块
IllegalUserName = 20101
UserCreateError = 20102
UserUpdateError = 20103
UserSearchError = 20104// 授权调用方
AuthorizedCreateError= 20201
AuthorizedListError= 20202
AuthorizedDeleteError= 20203
AuthorizedUpdateError= 20204
AuthorizedDetailError= 20205
AuthorizedCreateAPIError = 20206
AuthorizedListAPIError= 20207
AuthorizedDeleteAPIError = 20208// 管理员
AdminCreateError= 20301
AdminListError= 20302
AdminDeleteError= 20303
AdminUpdateError= 20304
AdminResetPasswordError= 20305
AdminLoginError= 20306
AdminLogOutError= 20307
AdminModifyPasswordError= 20308
AdminModifyPersonalInfoError = 20309// 配置
ConfigEmailError= 20401
ConfigSaveError= 20402
ConfigRedisConnectError = 20403
ConfigMySQLConnectError = 20404
ConfigMySQLInstallError = 20405
ConfigGoVersionError= 20406// 实用工具箱
SearchRedisError = 20501
ClearRedisError= 20502
SearchRedisEmpty = 20503
SearchMySQLError = 20504// 菜单栏
MenuCreateError = 20601
MenuUpdateError = 20602
MenuListError= 20603
MenuDeleteError = 20604
MenuDetailError = 20605// 借书
BookNotFoundError= 20701
BookHasBeenBorrowedError = 20702
)// 3、定义errorCode对应的文本信息
var codeTag = map[int]string{
ServerError:"Internal Server Error",
TooManyRequests:"Too Many Requests",
ParamBindError:"参数信息有误",
AuthorizationError: "签名信息有误",
CallHTTPError:"调用第三方 HTTP 接口失败",
ResubmitError:"Resubmit Error",
ResubmitMsg:"请勿重复提交",
HashIdsDecodeError: "ID参数有误",
SignatureError:"SignatureError",IllegalUserName: "非法用户名",
UserCreateError: "创建用户失败",
UserUpdateError: "更新用户失败",
UserSearchError: "查询用户失败",AuthorizedCreateError:"创建调用方失败",
AuthorizedListError:"获取调用方列表页失败",
AuthorizedDeleteError:"删除调用方失败",
AuthorizedUpdateError:"更新调用方失败",
AuthorizedDetailError:"获取调用方详情失败",
AuthorizedCreateAPIError: "创建调用方API地址失败",
AuthorizedListAPIError:"获取调用方API地址列表失败",
AuthorizedDeleteAPIError: "删除调用方API地址失败",AdminCreateError:"创建管理员失败",
AdminListError:"获取管理员列表页失败",
AdminDeleteError:"删除管理员失败",
AdminUpdateError:"更新管理员失败",
AdminResetPasswordError:"重置密码失败",
AdminLoginError:"登录失败",
AdminLogOutError:"退出失败",
AdminModifyPasswordError:"修改密码失败",
AdminModifyPersonalInfoError: "修改个人信息失败",ConfigEmailError:"修改邮箱配置失败",
ConfigSaveError:"写入配置文件失败",
ConfigRedisConnectError: "Redis连接失败",
ConfigMySQLConnectError: "MySQL连接失败",
ConfigMySQLInstallError: "MySQL初始化数据失败",
ConfigGoVersionError:"GoVersion不满足要求",SearchRedisError: "查询RedisKey失败",
ClearRedisError:"清空RedisKey失败",
SearchRedisEmpty: "查询的RedisKey不存在",
SearchMySQLError: "查询mysql失败",MenuCreateError: "创建菜单失败",
MenuUpdateError: "更新菜单失败",
MenuDeleteError: "删除菜单失败",
MenuListError:"获取菜单列表页失败",
MenuDetailError: "获取菜单详情失败",BookNotFoundError:"书未找到",
BookHasBeenBorrowedError: "书已经被借走了",
}func Text(code int) string {
return codeTag[code]
}// 4、新建自定义error实例化
func NewCustomError(code int) error {
// 初次调用得用Wrap方法,进行实例化
return errors.Wrap(&CustomError{
Code:code,
TagMessage: codeTag[code],
}, "")
}
3.3 自定义Error使用
新建测试文件:error_handler_test.go
package error_handleimport (
"fmt"
"github.com/pkg/errors"
"testing"
)func TestText(t *testing.T) {
books := []string{
"Book1",
"Book222222",
"Book3333333333",
}for _, bookName := range books {
err := searchBook(bookName)// 特殊业务场景:如果发现书被借走了,下次再来就行了,不需要作为错误处理
if err != nil {
// 提取error这个interface底层的错误码,一般在API的返回前才提取
// As - 获取错误的具体实现
var myError = new(CustomError)
// As - 解析错误内容
if errors.As(err, &myError) {
fmt.Printf("AS中的信息:当前书为: %s ,error code is %d, message is %s\n", bookName, myError.Code, myError.TagMessage)
}// 特殊场景,指定错误(ErrorBookHasBeenBorrowed)时,打印即可,不返回错误
// Is - 判断错误是否为指定类型
if errors.Is(err,NewCustomError(BookHasBeenBorrowedError)) {
fmt.Printf("IS中的信息:%s 已经被借走了, 只需按Info处理!\n", bookName)
err = nil
}else {
// 如果已有堆栈信息,应调用WithMessage方法
newErr := errors.WithMessage(err, "WithMessage err")
fmt.Printf("IS中的信息:%s 未找到,应该按Error处理! ,newErr is %s\n", bookName , newErr)
}
}
}
}func searchBook(bookName string) error {
// 1 发现图书馆不存在这本书 - 认为是错误,需要打印详细的错误信息
if len(bookName) > 10 {
return NewCustomError(BookHasBeenBorrowedError)
} else if len(bookName) > 6 {
// 2 发现书被借走了 - 打印一下被接走的提示即可,不认为是错误
return NewCustomError(BookHasBeenBorrowedError)
}
// 3 找到书 - 不需要任何处理
return nil
}
3.3 方式二:借助generate简化代码(建议使用) 方式一维护错误码与错误信息的关系较为复杂,我们可以借助go generate来自动生成代码。
3.3.1 安装stringer
stringer不是Go自带工具,需要手动安装。执行如下命令即可
go get golang.org/x/tools/cmd/stringer
3.3.1 定义错误信息
新建error_handler.go。在error_handler中,增加注释//go:generate stringer -type ErrCode -linecomment。执行go generate,会生成新的文件
文章图片
package error_handleimport (
"github.com/pkg/errors"
)// 1、自定义error结构体,并重写Error()方法
// 错误时返回自定义结构
type CustomError struct {
CodeErrCode `json:"code"`// 业务码
Message string`json:"message"` // 业务码
}func (e *CustomError) Error() string {
return e.Code.String()
}type ErrCode int64 //错误码// 2、定义errorCode
//go:generate stringer -type ErrCode -linecomment
const (
// 服务级错误码
ServerErrorErrCode = 10101 // Internal Server Error
TooManyRequestsErrCode = 10102 // Too Many Requests
ParamBindErrorErrCode = 10103 // 参数信息有误
AuthorizationError ErrCode = 10104 // 签名信息有误
CallHTTPErrorErrCode = 10105 // 调用第三方HTTP接口失败
ResubmitErrorErrCode = 10106 // ResubmitError
ResubmitMsgErrCode = 10107 // 请勿重复提交
HashIdsDecodeError ErrCode = 10108 // ID参数有误
SignatureErrorErrCode = 10109 // SignatureError// 业务模块级错误码
// 用户模块
IllegalUserName ErrCode = 20101 // 非法用户名
UserCreateError ErrCode = 20102 // 创建用户失败
UserUpdateError ErrCode = 20103 // 更新用户失败
UserSearchError ErrCode = 20104 // 查询用户失败// 配置
ConfigEmailErrorErrCode = 20401 // 修改邮箱配置失败
ConfigSaveErrorErrCode = 20402 // 写入配置文件失败
ConfigRedisConnectError ErrCode = 20403 // Redis连接失败
ConfigMySQLConnectError ErrCode = 20404 // MySQL连接失败
ConfigMySQLInstallError ErrCode = 20405 // MySQL初始化数据失败
ConfigGoVersionErrorErrCode = 20406 // GoVersion不满足要求// 实用工具箱
SearchRedisError ErrCode = 20501 // 查询RedisKey失败
ClearRedisErrorErrCode = 20502 // 清空RedisKey失败
SearchRedisEmpty ErrCode = 20503 // 查询的RedisKey不存在
SearchMySQLError ErrCode = 20504 // 查询mysql失败// 菜单栏
MenuCreateError ErrCode = 20601 // 创建菜单失败
MenuUpdateError ErrCode = 20602 // 更新菜单失败
MenuListErrorErrCode = 20603 // 删除菜单失败
MenuDeleteError ErrCode = 20604 // 获取菜单列表页失败
MenuDetailError ErrCode = 20605 // 获取菜单详情失败// 借书
BookNotFoundErrorErrCode = 20701 // 书未找到
BookHasBeenBorrowedError ErrCode = 20702 // 书已经被借走了
)// 4、新建自定义error实例化
func NewCustomError(code ErrCode) error {
// 初次调用得用Wrap方法,进行实例化
return errors.Wrap(&CustomError{
Code:code,
Message: code.String(),
}, "")
}
3.3.2 自定义Error使用
新建测试文件:error_handler_test.go
package error_handleimport (
"fmt"
"github.com/pkg/errors"
"testing"
)func TestText(t *testing.T) {
books := []string{
"Book1",
"Book222222",
"Book3333333333",
}for _, bookName := range books {
err := searchBook(bookName)// 特殊业务场景:如果发现书被借走了,下次再来就行了,不需要作为错误处理
if err != nil {
// 提取error这个interface底层的错误码,一般在API的返回前才提取
// As - 获取错误的具体实现
var customErr = new(CustomError)
// As - 解析错误内容
if errors.As(err, &customErr) {
//fmt.Printf("AS中的信息:当前书为: %s ,error code is %d, message is %s\n", bookName, customErr.Code, customErr.Message)
if customErr.Code == BookHasBeenBorrowedError {
fmt.Printf("IS中的info信息:%s 已经被借走了, 只需按Info处理!\n", bookName)
} else {
// 如果已有堆栈信息,应调用WithMessage方法
newErr := errors.WithMessage(err, "WithMessage err1")
// 使用%+v可以打印完整的堆栈信息
fmt.Printf("IS中的error信息:%s 未找到,应该按Error处理! ,newErr is: %+v\n", bookName, newErr)
}
}
}
}
}func searchBook(bookName string) error {
// 1 发现图书馆不存在这本书 - 认为是错误,需要打印详细的错误信息
if len(bookName) > 10 {
return NewCustomError(BookNotFoundError)
} else if len(bookName) > 6 {
// 2 发现书被借走了 - 打印一下被接走的提示即可,不认为是错误
return NewCustomError(BookHasBeenBorrowedError)
}
// 3 找到书 - 不需要任何处理
return nil
}
4 总结
CustomError
作为全局error
的底层实现,保存具体的错误码和错误信息;CustomError
向上返回错误时,第一次先用Wrap
初始化堆栈,后续用WithMessage
增加堆栈信息;- 从
error
中解析具体错误时,用errors.As
提取出CustomError
,其中的错误码和错误信息可以传入到具体的API接口中; - 要判断
error
是否为指定的错误时,用errors.Is
+Handler Error
的方法,处理一些特定情况下的逻辑;
Tips:
?
- 不要一直用errors.Wrap来反复包装错误,堆栈信息会爆炸,具体情况可自行测试了解
- 利用go generate可以大量简化初始化Erro重复的工作
github.com/pkg/errors
和标准库的error
完全兼容,可以先替换、后续改造历史遗留的代码- 一定要注意打印
error
的堆栈需要用%+v
,而原来的%v
依旧为普通字符串方法;同时也要注意日志采集工具是否支持多行匹配
我是简凡,一个励志用最简单的语言,描述最复杂问题的新时代农民工。求点赞,求关注,如果你对此篇文章有什么疑惑,欢迎在我的微信公众号中留言,我还可以为你提供以下帮助:
【最佳实践之Golang错误处理】我的博客:https://besthpt.github.io/
- 帮助建立自己的知识体系
- 互联网真实高并发场景实战讲解
- 不定期分享Golang、Java相关业内的经典场景实践
微信公众号:
文章图片
推荐阅读
- PMSJ寻平面设计师之现代(Hyundai)
- 太平之莲
- 闲杂“细雨”
- 七年之痒之后
- 深入理解Go之generate
- 由浅入深理解AOP
- 期刊|期刊 | 国内核心期刊之(北大核心)
- 生活随笔|好天气下的意外之喜
- 感恩之旅第75天
- python学习之|python学习之 实现QQ自动发送消息