Go错误处理规范化
tbghg

参考

kuriball大佬公司内的文档

处理方式

目前在进行一个业务的错误处理优化,方便后续快速定位问题。

错误处理不当主要体现在以下方面:

  1. 没有打印堆栈信息,定位问题困难
  2. 重复携带堆栈信息,冗余
  3. 一个error打印多次(例如每一层都打印)

首先看几个和 日志 / error 相关的库

  1. errors标准库
  2. github.com/pkg/errors (下文直接称为pkg-errors)
  3. 公司内部 ecode(error code)
  4. 公司内部 log

堆栈信息是排查问题的一个重要手段,例如:

package main

import (
    "fmt"

    "github.com/pkg/errors"
)

func main() {
    err := errors.New("whoops")
    fmt.Printf("%+v", err)

    // Example output:
    // whoops
    // github.com/pkg/errors_test.ExampleNew_printf
    //         /home/dfc/src/github.com/pkg/errors/example_test.go:17
    // testing.runExample
    //         /home/dfc/go/src/testing/example.go:114
    // testing.RunExamples
    //         /home/dfc/go/src/testing/example.go:38
    // testing.(*M).Run
    //         /home/dfc/go/src/testing/testing.go:744
    // main.main
    //         /github.com/pkg/errors/_test/_testmain.go:106
    // runtime.main
    //         /home/dfc/go/src/runtime/proc.go:183
    // runtime.goexit
    //         /home/dfc/go/src/runtime/asm_amd64.s:2059
}

需要注意格式化输出error时,%v只会输出错误文本,stack

根据堆栈信息我们可以快速定位到日志位置和调用链路

不携带堆栈信息方法:

  • errors标准库 - errors.New
  • pkg-errors - errors.WithMessage
  • 公司内部的 ecode 创建error

携带堆栈信息的方法:

  • pkg-errors 的 errors.New
  • pkg-errors / errors - errors.Wrap
  • pkg-errors - errors.WithStack

直接看源码即可,注释也很详细

公司内部在web框架中加入了中间件,会统一打印日志,推荐先把中间件的代码过一下,会更清楚整个流程

错误打印原则:

  1. 最底层(比如公共库)返回原始错误, 无需wrap
  2. 应用下层流转错误, 使用wrap带上堆栈
  3. 应用内方法互相调用, 避免重复wrap导致重复堆栈, WithMessage携带信息即可

公共库返回的error是否携带堆栈信息?

可以看下源码,例如json.Marshal

func Marshal(v any) ([]byte, error) {
    e := newEncodeState()
    defer encodeStatePool.Put(e)

    err := e.marshal(v, encOpts{escapeHTML: true})
    if err != nil {
        return nil, err
    }
    buf := append([]byte(nil), e.Bytes()...)

    return buf, nil
}

......

// jsonError is an error wrapper type for internal use only.
// Panics with errors are wrapped in jsonError so that the top-level recover
// can distinguish intentional panics from this package.
type jsonError struct{ error }

func (e *encodeState) marshal(v any, opts encOpts) (err error) {
    defer func() {
        if r := recover(); r != nil {
            if je, ok := r.(jsonError); ok {
                err = je.error
            } else {
                panic(r)
            }
        }
    }()
    e.reflectValue(reflect.ValueOf(v), opts)
    return nil
}

先不去处理错误,发生panic后recover,从里面提取原因并转换为jsonError,一般发生panic后会程序崩溃会输出堆栈信息,但recover中只能获得报错信息,想要获取堆栈信息可以通过debug.Stack(),例如下面的代码:

package main

import (
    "fmt"
    "runtime/debug"
)

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("panic: %s\nstack trace:\n%s", r, debug.Stack())
        }
    }()
    foo()
}

func foo() {
    panic("oh foo")
}
/* output:
panic: oh foo
stack trace:
goroutine 1 [running]:
runtime/debug.Stack()
    /usr/local/go-faketime/src/runtime/debug/stack.go:24 +0x65
main.main.func1()
    /tmp/sandbox1024588529/prog.go:11 +0x39
panic({0x48a440, 0x4b83c8})
    /usr/local/go-faketime/src/runtime/panic.go:884 +0x213
main.foo(...)
    /tmp/sandbox1024588529/prog.go:18
main.main()
    /tmp/sandbox1024588529/prog.go:14 +0x4a
*/

所以json.Marshal函数是不会输出堆栈信息,如果出现error了需要Wrap或者WithStack一下

wrap 或 WithMessage 后会不会影响到ecode对于前端的提示?

前端能够正确展示对应的提示是基于返回的ecode中的code和message, 前端展示message信息。

  • wrap之前: 应用返回原始error(即为根因error), 如果该error为ecode, 拥有对应的code和message, 则前端能正常展示, 否则则不能
  • wrap之后: 应用层通用返回原始error(即为根因error), wrap加上堆栈, 出口处框架会自动cause寻找根因, 之后和wrap之前逻辑一致
    也就是说, 重点在于返回的原始error是否为ecode才是重点, 与是否wrap没有关系

例如:

// 方式一
if err != nil {
    log.Errorc(c, "AddQASnapshot addQASnapshotDB error(%v) oid(%s)", err, req.Oid)
    err = qaEcode.QASnapshotAddErr
}
// 方式二
if err != nil {
    err = errors.WithMessagef(qaEcode.QASnapshotAddErr, "AddQASnapshot addQASnapshotDB oid(%s), error:%v", err, req.Oid)
}

两种方法都可以,方式一中,先将系统内的错误打印出来方便程序员问题定位,然后通过ecode替换为对应error,为用户展示错误信息。方式二是将qaEcode.QASnapshotAddErr作为原始error,将错误信息加到msg中,最后通过中间件统一打印。

举个例子更加直观一些:

package main

import (
    "fmt"
    "github.com/pkg/errors"
)

func main() {
    err := errors.New("这是个err")
    err2 := errors.New("这是个err2")
    err3 := errors.WithMessagef(err, "err2:%+v", err2)
    fmt.Printf("%+v\n", err3)
    fmt.Println("errors.Cause:", errors.Cause(err3))
}

/* output:
这是个err
main.main
    /tmp/sandbox4241311282/prog.go:9
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:250
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1598
err2:这是个err2
main.main
    /tmp/sandbox4241311282/prog.go:10
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:250
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1598
errors.Cause: 这是个err
*/
 评论