关于golang的error设计
关于golang error的设计,Gophers 应该都知道有两派观点:
- 代码中充斥大量的错误处理,增加工作量,看起来烦。
- 显示的处理错误可以快速定位问题,效率高。
两派观点都能理解,但是官方最终一锤定音,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异常方案来对比的。这些都不重要,重要的是维护性要高,少搞屎山。线上问题能够快速定位、人员流动能快速接手、需求变更能快速迭代的代码才是优先考虑的。