前言
Golang在错误处理上,没有形成良好的规范,导致真正用好的人非常少,大部分golang开发人员(哪怕是3年+)在错误处理上,依旧无法避免以下问题:
- 1.单条错误链路过长。
err.Elem("用户模块").Text("用户查询信息异常").Stack(debug.Stack()).Attach(map[string]interface{}{
"url": c.FullPath(),
"param": param,
})
- 2.同种错误,多次处理。
control/login.go
func Login(c *gin.Context) error {
if e:=service.Login(userId);e!=nil {
logging.Println(e)
return
}
}
service/login.go
func Login(userId int) error {
if e:= dao.Login(userId);e!=nil {
logging.Println(e)
return e
}
return nil
}
- 3.链路过于复杂,包含太多底层链路
runtime/debug.Stack(0x7deb80, 0xc000006018, 0xc000063f58)
E:/go1.12/src/runtime/debug/stack.go:24 +0xa4
github.com/fwhezfwhez/errorx.TestSe(0xc0000ca100)
G:/go_workspace/GOPATH/src/errorX/errorx_test.go:334 +0x33b
testing.tRunner(0xc0000ca100, 0x78aad8)
E:/go1.12/src/testing/testing.go:865 +0xc7
created by testing.(*T).Run
E:/go1.12/src/testing/testing.go:916 +0x361
testing.tRunner(0xc0000ca100, 0x78aad8)
E:/go1.12/src/testing/testing.go:865 +0xc7
created by testing.(*T).Run
E:/go1.12/src/testing/testing.go:916 +0x361
testing.tRunner(0xc0000ca100, 0x78aad8)
E:/go1.12/src/testing/testing.go:865 +0xc7
created by testing.(*T).Run
E:/go1.12/src/testing/testing.go:916 +0x361
testing.tRunner(0xc0000ca100, 0x78aad8)
E:/go1.12/src/testing/testing.go:865 +0xc7
created by testing.(*T).Run
E:/go1.12/src/testing/testing.go:916 +0x361
testing.tRunner(0xc0000ca100, 0x78aad8)
E:/go1.12/src/testing/testing.go:865 +0xc7
created by testing.(*T).Run
- 4.外围错误枚举判断
if e !=nil {
if e == loginService.LoginPasswordWrongErr {
c.JSON(200, gin.H{"errmsg":e.Error(), "errcode":1})
return
}
if e == loginService.LoginInvalidUsernameErr {
c.JSON(200, gin.H{"errmsg":e.Error(), "errcode":2})
return
}
if e == loginService.LoginFrequencyErr {
c.JSON(200, gin.H{"errmsg":e.Error(), "errcode":3})
return
}
...
c.JSON(200, gin.H{"errmsg":"系统错误", "errcode":10005})
return
}
- 5.循环打标
func PlayGame() {
e := handle1()
if e!=nil {
err.SaveErr(e, map[string]interface{}{
"label":"xyx:game",
"elem": "game"
})
e = handle2()
if e!=nil {
err.SaveErr(e, map[string]interface{}{
"label":"xyx:game",
"elem": "game"
})
e = handle3()
if e!=nil {
err.SaveErr(e, map[string]interface{}{
"label":"xyx:game",
"elem": "game"
})
e = handle4()
if e!=nil {
err.SaveErr(e, map[string]interface{}{
"label":"xyx:game",
"elem": "game"
})
e = handle5()
if e!=nil {
err.SaveErr(e, map[string]interface{}{
"label":"xyx:game",
"elem": "game"
})
}
除此之外呢,还有一些:
- 6.必须上线才能看到日志(没有权限的人只能靠猜)。
- 7.必须打开多个服务器同时看日志(因为应用组不止一个服务节点)
- 8.只记载了同类型错误的积累次数,无法定位每条错误(防止打库频繁)
本次,将对上述的1-5问题,提供有效的解决方案。对6-8问题,提供技术方向。
解决方案
- 使用 github.com/fwhezfwhez/errorx 开源包。
1. 单条链路过长
在使用时,会接入项目里的错误报警机制,每一个需求/模块,会通过【代码生成】提供附加链路的方法包。
---login
| -- loginModel
| -- loginService
| -- loginControl
| -- loginRouter
| -- loginUtil
| |--error.go
error.go
func SaveError(e error, ctx ...map[string]interface{}) {
if len(ctx) == 0 {
ctx = []map[string]interface{}{
{
"label": "xyx:login",
"elem": "xyx:game",
},
}
} else {
ctx[0]["label"] = "xyx:login"
ctx[0]["elem"] = "xyx:game"
}
errs.SaveError(errorx.Wrap(e), ctx...)
}
每个错误处理的顶层,只需要调用
xxxUtil.SaveError(errorx.Wrap(e))
2. 同种错误多次处理
- 所有错误统一在control里处理,其他包下的错误,一律return errorx.Wrap(e)
if e:= a.handle();e!=nil {
return errorx.Wrap(e)
}
3. 错误链路过于复杂
- 只会打印wrap处的行号,不会载入太深的底层链路
func loginwrap() error {
e := fmt.Errorf("time out")
return errorx.Wrap(e)
}
func main() {
if e:= loginwrap() {
fmt.Println(errorx.Wrap(e).Error())
return
}
}
输出:
/x/x/x/x/main:15 | time out
/x/x/x/x/main:9 | time out
4. 业务错误枚举过多
- 对ServiceError自动输出errmsg和errcode,而不需要枚举对比。
func Login() error {
return errorx.NewServiceError("登录密码错误",1)
// return errorx.NewServiceError("账户重复",2)
// return errorx.NewServiceError("登录频繁",3)
}
func main() {
e := Login()
if se,ok := errorx.IsServiceError(e); ok {
fmt.Println(se.Errmsg, se.Errcode)
}
}
5. 循环打标
和第一点类似,通过【自动生成】的错误处理包来自动打上需求和模块标签,业务中只需要顶层处理就好了
err.SaveError(errorx.Wrap(e))
6. 必须上线才能看日志
需要对错误提供上报机制,应用组统一上报到同一个数据库(通过上报方限频,mq异步限制消费速率,数据库hash代理,来保证数据库稳定)。
对错误信息提供后台接口查询,避免上服务器查询。
7. 必须打开多个服务器同时看日志
同6
8. 错误只记录了次数和最新一条详情
需要对每个标签的每条任务都做好存储,期限最好保留7天以上,并且,对订制标签需要做到报警机制。