前言

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.Wrap(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天以上,并且,对订制标签需要做到报警机制。