解决方案(10) golang错误处理

前言

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

上一篇:多个站点同时抓取!怎么做到的?


下一篇:Go - 常用签名算法的基准测试