星火九问 之 一种 Golang Error 业务设计实践

44 阅读4分钟

关于golang的error设计

关于golang error的设计,Gophers 应该都知道有两派观点:

  1. 代码中充斥大量的错误处理,增加工作量,看起来烦。
  2. 显示的处理错误可以快速定位问题,效率高。

两派观点都能理解,但是官方最终一锤定音,error 的机制就这样定了,爱咋咋地。因为官方的意思,就是希望Gophers通过显示的错误处理逻辑,让程序更加健壮。

本文今天聊聊 golang error 的中的工程理念,并提供一种工程化的设计思路。

error 的核心作用到底是什么呢?

error是给人看代码用的

对,它既不是给代码控制流程用,也不是给计算机运行用,也不是给线上服务调度用,它就是给人看代码用的。 这个理由,我认为也是golang error官方设计思路的本质原因。

error 本质上就是一个普通的值对象,只是恰好被选中,承担起标识业务代码中出现问题的具体位置和上下文的命运。

golang也没要求一定要返回error,像其他编程语言 C、Java那样返回单值列也没啥问题。 不过话说回来,这些语言也一样对返回值进行错误判断和处理啊。 要我看,还不如Golang这样显示的错误处理呢。

但是,也没必要所有编写的函数,都要返回error,那样比较臃肿。

分代码要划分类别,不同类别编程理念和侧重点是有区别的。 比如业务类代码,重点是易读、易修改的维护性,那么明确error当然实现流程更加清晰,减少隐藏逻辑。 比如算法类代码,固定的输出有固定的输出,测试通过就不会出问题,这种情况下,error就可以省略。

一种error的设计实践

既然error目的是给人看,那么当然要思考怎么“看”error了。 说来简单,无非就是与logging、metrics、tracing三板斧的结合。 实现思路很简单,利用Error Wrapper思路,在调用路径重点处打标。最后在框架中间件,或者Controller层统一处理Error。 目前,如下实现可以实现在 业务流程代码中不包含任何的日志打印的语句。

package biz_error

import (
    "dgolang/utils/asserts"
    "errors"
    "fmt"
)

type BizError interface {
    Error() string                      // 输出起点错误Error
    Wrapper(msg string) BizError        // 可以对上层进行包装
    ErrorCode() ErrorCode               // 获取错误码
    ErrorMsg() string                   // 获取错误链条输出
    WithError(err interface{}) BizError // 支持自定义Error
    IsBizError() bool                   // 判断是否是业务类型错误
    IsInnerError() bool                 // 判断是否是内部错误
}

type Kind int

const (
    BizKind      Kind = 1
    InternalKind Kind = 2
)

type TError struct {
    code ErrorCode
    msg  string 
    err  error 
    kind Kind 
}

func (e TError) IsBizError() bool {
    return e.kind == BizKind
}
func (e TError) IsInnerError() bool {
    return e.kind == InternalKind
}
func (e TError) Error() string {
    return e.err.Error()
}

func (e TError) Wrapper(msg string) BizError {
    if len(msg) == 0 {
       return e
    }
    asserts.MustTrue(e.code != 0)

    var wrapMsg string
    if len(e.msg) != 0 {
       wrapMsg = fmt.Sprintf("%s <-> %s", msg, e.msg)
    } else {
       wrapMsg = msg
    }

    return TError{
       code: e.code,
       msg:  wrapMsg,
       err:  e.err,
       kind: e.kind,
    }
}

func (e TError) ErrorCode() ErrorCode {
    return e.code
}

func (e TError) ErrorMsg() string { return e.msg }

func (e TError) WithError(err interface{}) BizError {
    et, ok := err.(error)
    asserts.MustTrue(ok)
    return TError{
       code: e.code,
       msg:  et.Error(),
       kind: e.kind,
       err:  err.(error),
    }
}

func NewBizError(code ErrorCode, msg string) BizError {
    return TError{
       code: code,
       msg:  msg,
       err:  errors.New(msg),
       kind: BizKind,
    }
}

func NewGRPCError(err error) BizError {
    return TError{
       code: 1,
       msg:  err.Error(),
       err:  err,
       kind: InternalKind,
    }
}

var ArgumentError = NewBizError(1001, "argument error")

调用方代码


func Func1() biz_error.BizError {
    return biz_error.ArgumentError.Wrapper("list is too long")
}

func Func2() biz_error.BizError {
    err := fmt.Errorf("May be panic")
    return biz_error.NewGRPCError(err)
}

func Func3() biz_error.BizError {
    err := Func1()
    return err.Wrapper("func 3 Caller")
}

func Func4() biz_error.BizError {
    err := Func2()
    return err.Wrapper("func 4 Caller")
}

// 一般来说,框架都有中间件,可以直接在中间件中完成Error的分析。
func MonitorError(err biz_error.BizError) {
    if err == nil {return}
    if err.IsInnerError() {
       tmetrics.InternalErrorTotalVec.WithLabelValues(string(err.ErrorCode())).Inc() // 上报大盘监控。
       tlog.Logger().Errorf("inner error: %v", err.ErrorMsg())                      // 本地错误日志
    } else {
       tlog.Logger().Infof("biz error: %v", err.ErrorMsg()) // 打印日志
    }
}


func main() {

    MonitorError(Func3())
    MonitorError(Func4())
}

// 输出。每一次调用关系,使用“<->” 进行分割。
time="2025-12-12 15:04:17" level=info msg="biz error: func 3 Caller <-> list is too long <-> argument error" func=main.MonitorError
time="2025-12-12 15:04:17" level=error msg="inner error: func 4 Caller <-> May be panic" func=main.MonitorError 


总结

看到有一些吐槽error设计的,比如增加了工作量,或者增加了无效代码,甚至还有用Java异常方案来对比的。这些都不重要,重要的是维护性要高,少搞屎山。线上问题能够快速定位、人员流动能快速接手、需求变更能快速迭代的代码才是优先考虑的。