1、原生错误处理
Go 语言通过内置的错误接口提供了非常简单的错误处理机制。 error类型是一个接口类型,这是它的定义:
typeerrorinterface{Error()string}
我们可以在编码中通过实现 error 接口类型来生成错误信息。 函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:
funcSqrt(ffloat64)(float64,error){iff<0{return0,errors.New("math:squarerootofnegativenumber")}//实现}
在下面的例子中,我们在调用Sqrt的时候传递的一个负数,然后就得到了non-nil的error对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误,请看下面调用的示例代码:
result,err:=Sqrt(-1)iferr!=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
packageerror_handleimport("github.com/pkg/errors")//1、自定义error结构体,并重写Error()方法//错误时返回自定义结构typeCustomErrorstruct{Codeint`json:"code"`//业务码TagMessagestring`json:"message"`//描述信息}func(e*CustomError)Error()string{returne.TagMessage}//2、定义errorCodeconst(//服务级错误码ServerError=10101TooManyRequests=10102ParamBindError=10103AuthorizationError=10104CallHTTPError=10105ResubmitError=10106ResubmitMsg=10107HashIdsDecodeError=10108SignatureError=10109//业务模块级错误码//用户模块IllegalUserName=20101UserCreateError=20102UserUpdateError=20103UserSearchError=20104//授权调用方AuthorizedCreateError=20201AuthorizedListError=20202AuthorizedDeleteError=20203AuthorizedUpdateError=20204AuthorizedDetailError=20205AuthorizedCreateAPIError=20206AuthorizedListAPIError=20207AuthorizedDeleteAPIError=20208//管理员AdminCreateError=20301AdminListError=20302AdminDeleteError=20303AdminUpdateError=20304AdminResetPasswordError=20305AdminLoginError=20306AdminLogOutError=20307AdminModifyPasswordError=20308AdminModifyPersonalInfoError=20309//配置ConfigEmailError=20401ConfigSaveError=20402ConfigRedisConnectError=20403ConfigMySQLConnectError=20404ConfigMySQLInstallError=20405ConfigGoVersionError=20406//实用工具箱SearchRedisError=20501ClearRedisError=20502SearchRedisEmpty=20503SearchMySQLError=20504//菜单栏MenuCreateError=20601MenuUpdateError=20602MenuListError=20603MenuDeleteError=20604MenuDetailError=20605//借书BookNotFoundError=20701BookHasBeenBorrowedError=20702)//3、定义errorCode对应的文本信息varcodeTag=map[int]string{ServerError:"InternalServerError",TooManyRequests:"TooManyRequests",ParamBindError:"参数信息有误",AuthorizationError:"签名信息有误",CallHTTPError:"调用第三方HTTP接口失败",ResubmitError:"ResubmitError",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:"书已经被借走了",}funcText(codeint)string{returncodeTag[code]}//4、新建自定义error实例化funcNewCustomError(codeint)error{//初次调用得用Wrap方法,进行实例化returnerrors.Wrap(&CustomError{Code:code,TagMessage:codeTag[code],},"")}
3.3 自定义Error使用
新建测试文件:error_handler_test.go
packageerror_handleimport("fmt""github.com/pkg/errors""testing")funcTestText(t*testing.T){books:=[]string{"Book1","Book222222","Book3333333333",}for_,bookName:=rangebooks{err:=searchBook(bookName)//特殊业务场景:如果发现书被借走了,下次再来就行了,不需要作为错误处理iferr!=nil{//提取error这个interface底层的错误码,一般在API的返回前才提取//As-获取错误的具体实现varmyError=new(CustomError)//As-解析错误内容iferrors.As(err,&myError){fmt.Printf("AS中的信息:当前书为:%s,errorcodeis%d,messageis%s\n",bookName,myError.Code,myError.TagMessage)}//特殊场景,指定错误(ErrorBookHasBeenBorrowed)时,打印即可,不返回错误//Is-判断错误是否为指定类型iferrors.Is(err,NewCustomError(BookHasBeenBorrowedError)){fmt.Printf("IS中的信息:%s已经被借走了,只需按Info处理!\n",bookName)err=nil}else{//如果已有堆栈信息,应调用WithMessage方法newErr:=errors.WithMessage(err,"WithMessageerr")fmt.Printf("IS中的信息:%s未找到,应该按Error处理!,newErris%s\n",bookName,newErr)}}}}funcsearchBook(bookNamestring)error{//1发现图书馆不存在这本书-认为是错误,需要打印详细的错误信息iflen(bookName)>10{returnNewCustomError(BookHasBeenBorrowedError)}elseiflen(bookName)>6{//2发现书被借走了-打印一下被接走的提示即可,不认为是错误returnNewCustomError(BookHasBeenBorrowedError)}//3找到书-不需要任何处理returnnil}
3.3 方式二:借助generate简化代码(建议使用)
方式一维护错误码与错误信息的关系较为复杂,我们可以借助go generate来自动生成代码。
3.3.1 安装stringer
stringer不是Go自带工具,需要手动安装。执行如下命令即可
gogetgolang.org/x/tools/cmd/stringer
3.3.1 定义错误信息
新建error_handler.go。在error_handler中,增加注释//go:generate stringer -type ErrCode -linecomment。执行go generate,会生成新的文件
packageerror_handleimport("github.com/pkg/errors")//1、自定义error结构体,并重写Error()方法//错误时返回自定义结构typeCustomErrorstruct{CodeErrCode`json:"code"`//业务码Messagestring`json:"message"`//业务码}func(e*CustomError)Error()string{returne.Code.String()}typeErrCodeint64//错误码//2、定义errorCode//go:generatestringer-typeErrCode-linecommentconst(//服务级错误码ServerErrorErrCode=10101//InternalServerErrorTooManyRequestsErrCode=10102//TooManyRequestsParamBindErrorErrCode=10103//参数信息有误AuthorizationErrorErrCode=10104//签名信息有误CallHTTPErrorErrCode=10105//调用第三方HTTP接口失败ResubmitErrorErrCode=10106//ResubmitErrorResubmitMsgErrCode=10107//请勿重复提交HashIdsDecodeErrorErrCode=10108//ID参数有误SignatureErrorErrCode=10109//SignatureError//业务模块级错误码//用户模块IllegalUserNameErrCode=20101//非法用户名UserCreateErrorErrCode=20102//创建用户失败UserUpdateErrorErrCode=20103//更新用户失败UserSearchErrorErrCode=20104//查询用户失败//配置ConfigEmailErrorErrCode=20401//修改邮箱配置失败ConfigSaveErrorErrCode=20402//写入配置文件失败ConfigRedisConnectErrorErrCode=20403//Redis连接失败ConfigMySQLConnectErrorErrCode=20404//MySQL连接失败ConfigMySQLInstallErrorErrCode=20405//MySQL初始化数据失败ConfigGoVersionErrorErrCode=20406//GoVersion不满足要求//实用工具箱SearchRedisErrorErrCode=20501//查询RedisKey失败ClearRedisErrorErrCode=20502//清空RedisKey失败SearchRedisEmptyErrCode=20503//查询的RedisKey不存在SearchMySQLErrorErrCode=20504//查询mysql失败//菜单栏MenuCreateErrorErrCode=20601//创建菜单失败MenuUpdateErrorErrCode=20602//更新菜单失败MenuListErrorErrCode=20603//删除菜单失败MenuDeleteErrorErrCode=20604//获取菜单列表页失败MenuDetailErrorErrCode=20605//获取菜单详情失败//借书BookNotFoundErrorErrCode=20701//书未找到BookHasBeenBorrowedErrorErrCode=20702//书已经被借走了)//4、新建自定义error实例化funcNewCustomError(codeErrCode)error{//初次调用得用Wrap方法,进行实例化returnerrors.Wrap(&CustomError{Code:code,Message:code.String(),},"")}
3.3.2 自定义Error使用
新建测试文件:error_handler_test.go
packageerror_handleimport("fmt""github.com/pkg/errors""testing")funcTestText(t*testing.T){books:=[]string{"Book1","Book222222","Book3333333333",}for_,bookName:=rangebooks{err:=searchBook(bookName)//特殊业务场景:如果发现书被借走了,下次再来就行了,不需要作为错误处理iferr!=nil{//提取error这个interface底层的错误码,一般在API的返回前才提取//As-获取错误的具体实现varcustomErr=new(CustomError)//As-解析错误内容iferrors.As(err,&customErr){//fmt.Printf("AS中的信息:当前书为:%s,errorcodeis%d,messageis%s\n",bookName,customErr.Code,customErr.Message)ifcustomErr.Code==BookHasBeenBorrowedError{fmt.Printf("IS中的info信息:%s已经被借走了,只需按Info处理!\n",bookName)}else{//如果已有堆栈信息,应调用WithMessage方法newErr:=errors.WithMessage(err,"WithMessageerr1")//使用%+v可以打印完整的堆栈信息fmt.Printf("IS中的error信息:%s未找到,应该按Error处理!,newErris:%+v\n",bookName,newErr)}}}}}funcsearchBook(bookNamestring)error{//1发现图书馆不存在这本书-认为是错误,需要打印详细的错误信息iflen(bookName)>10{returnNewCustomError(BookNotFoundError)}elseiflen(bookName)>6{//2发现书被借走了-打印一下被接走的提示即可,不认为是错误returnNewCustomError(BookHasBeenBorrowedError)}//3找到书-不需要任何处理returnnil}
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、Java相关业内的经典场景实践
我的博客:https://besthpt.github.io/
微信公众号:"简凡丶"