Go中应通过自定义实现Error()和Unwrap()方法的结构体添加错误上下文,配合errors.Is/As进行类型判断,优先使用fmt.Errorf("%w")包装而非字符串拼接,避免用fmt.Sprint(err)做类型判断。

Go 里怎么让错误带上下文信息
直接用 errors.New 或 fmt.Errorf 生成的错误是“哑巴错误”——没字段、不能判断类型、加不了追踪 ID。真要区分“文件不存在”和“权限不足”,或者想在日志里打上请求 ID,就得自定义类型。
最稳妥的做法是定义一个结构体,实现 Error() 方法:
type MyError struct {
Code int
Message string
ReqID string
}
func (e *MyError) Error() string {
return fmt.Sprintf("[%s] code=%d: %s", e.ReqID, e.Code, e.Message)
}
- 必须是指针接收者(否则值拷贝后修改
ReqID不生效) - 别忘了导出字段名(
Code而不是code),否则外部没法读取 - 如果只做类型判断(比如
if errors.As(err, &e) {...}),方法签名必须严格匹配:接收者是*MyError,不是MyError
用 errors.Is 和 errors.As 判断自定义错误
不用再写 strings.Contains(err.Error(), "timeout") 这种脆弱逻辑。只要你的错误实现了 Unwrap() 方法,就能接入 Go 标准错误链体系。
常见写法:
立即学习“go语言免费学习笔记(深入)”;
func (e *MyError) Unwrap() error {
return e.cause // 指向原始错误,可以是 nil
}
-
errors.Is(err, myTimeoutErr)成立的前提:你得定义一个包级变量var ErrTimeout = &MyError{Code: 408},且比较时用指针 -
errors.As(err, &e)要求e是对应类型的指针变量,不是值(var e MyError会失败) - 如果错误链里有多层包装,
Unwrap()必须返回下一层,不能返回nil后就断掉
嵌套标准错误时别漏掉 fmt.Errorf("%w", err)
自己造轮子不如复用。多数时候,你只是想给 os.Open 的错误加个前缀或字段,而不是从零写个新错误类型。
- 用
%w而不是%s包装:它会保留原始错误的可展开性,支持errors.Is/As - 错误链里只能有一个
%w,多了会 panic;没有%w就变成“断链”,上游无法追溯根因 - 别在
%w后面再拼字符串(如fmt.Errorf("failed to read: %w: %s", err, detail)),这会让Unwrap()返回的不是原始错误
为什么不要用 fmt.Sprint(err) 做类型判断
有人把错误转成字符串再 strings.HasPrefix,这等于放弃 Go 1.13+ 的错误处理基础设施。
- 字符串匹配对大小写、空格、冒号位置极度敏感,一次日志格式调整就让判断失效
- 跨服务传递错误时(比如 gRPC),原始错误类型信息早已丢失,只剩字符串描述
-
fmt.Sprint(err)会触发Error()方法,如果该方法有副作用(比如打日志、调 API),就会重复执行
真正需要调试时,用 fmt.Printf("%+v", err) 看完整错误链;生产环境判断,只依赖 errors.Is 和结构体字段。


















