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









