Go错误链靠%w包装和errors.Is/As识别,自定义错误需实现Unwrap()才能穿透;裸错无上下文,字符串匹配不可靠,runtime.Caller性能差且不反映传播路径。

Go里多个errors.New或fmt.Errorf混在一起,怎么知道哪个是哪层抛的?
靠错误值本身无法区分——Go原生error接口只保证Error() string方法,所有错误都扁平成字符串。一旦多处用fmt.Errorf("failed to %s: %w", op, err)层层包装,err.Error()会连成一长串,但调用栈、原始错误类型、触发位置全被抹掉。
真正能定位问题的是「错误链」+「上下文标记」,不是靠Tag(Go里没有error Tag机制,容易和struct tag混淆)。
- 别用
errors.New("db timeout")裸错,它没上下文,也没法判断是否由某次HTTP调用引发 - 避免在中间层用
fmt.Errorf("handle request failed: %v", err)——丢失了%w,错误链就断了 - 如果必须加标识,用带字段的自定义错误类型,而不是靠字符串关键词匹配(比如搜"db"或"http")
用fmt.Errorf("%w")保持错误链,再配合errors.Is/errors.As精准识别
Go 1.13+ 的错误链机制才是解法核心:%w让错误可嵌套,errors.Is按值判断是否为某个底层错误,errors.As按类型提取原始错误。
例如你有三个可能出错环节:HTTP请求、JSON解析、DB写入,各自返回不同错误类型:
立即学习“go语言免费学习笔记(深入)”;
type HTTPError struct{ Code int; URL string }
func (e *HTTPError) Error() string { return fmt.Sprintf("http %d from %s", e.Code, e.URL) }
type ParseError struct{ Msg string }
func (e *ParseError) Error() string { return "json parse failed: " + e.Msg }
调用时用%w包装:
if err := doHTTP(); err != nil {
return fmt.Errorf("calling api /users: %w", err) // ← 保留链
}
下游就能准确识别:
-
errors.Is(err, &HTTPError{})→ 判断是不是HTTP层错 -
var httpErr *HTTPError; errors.As(err, &httpErr)→ 提取并访问httpErr.Code - 即使中间包了5层
fmt.Errorf(... %w),也能穿透到底层
为什么不用runtime.Caller手动打栈?
有人会想:我每次errors.New前记下runtime.Caller(1),把文件/行号塞进错误字符串里——这确实能看位置,但代价很高:
- 每次创建错误都要做PC解析,影响性能(尤其高频路径如日志、校验)
- 栈信息是静态快照,无法反映实际传播路径(比如A→B→C报错,但你在B层打的栈只显示B,看不到C)
- 字符串里的路径在跨服务、跨进程时容易失效(路径不一致、符号被strip)
真正需要栈信息时,应该用debug.PrintStack()或集成github.com/pkg/errors(虽已归档,但逻辑清晰),或者直接用errors.WithStack(需自行实现)——但注意:它只应在调试/告警场景启用,非默认行为。
自定义错误类型里要不要加Unwrap()?
要,但只在你希望它参与标准错误链时才加。如果你的错误类型只是用来errors.As提取,且不打算被别人再%w包装,那可以不实现Unwrap;但一旦你用fmt.Errorf("xxx: %w", myErr),而myErr没Unwrap(),那整个链就断在它这儿了。
正确写法:
type DBError struct {
Query string
Err error // 原始错误,可能来自driver
}
func (e *DBError) Error() string { return "db exec failed: " + e.Query }
func (e *DBError) Unwrap() error { return e.Err } // ← 关键:让errors.Is/As能穿透
漏掉Unwrap()是最常踩的坑——错误看起来能传下去,但errors.As(err, &target)永远失败,因为链在你的类型上卡死了。
复杂点在于:错误链不是线性的,而是树状的;Unwrap()只能返回一个error,所以设计自定义错误时得想清楚“这个错误的核心根源是什么”,别为了塞多个原因强行拼接。


















