

新闻资讯
技术教程Go 1.20+ 错误链不会自动展开多层,需手动调用 errors.Unwrap 或 errors.Is/As 才逐层解包;%w 是唯一建立可解包包装关系的方式,不用则丢失原始错误;可通过 flattenErr 截断深度,%+v 查看链,注意循环引用。
不需要手动包装多层,fmt.Errorf 默认只保留一层包装(除非显式用 %w)。错误链的“深度”由你调用 fmt.Errorf("...", err) 的次数决定,不是语言自动叠加的。Go 运行时不会递归展开嵌套错误——它只在你调用 errors.Unwrap 或遍历 errors.Is/errors.As 时才逐层解包。
%w,什么时候不该用?%w 是唯一能建立可被 errors.Unwrap 识别的包装关系的操作。不用 %w(比如写成 fmt.Errorf("failed: %v", err))会丢失原始错误,变成纯字符串拼接,后续无法用 errors.Is 判断底层原因。
return fmt.Errorf("read config: %w", io.EOF)return fmt.Errorf("read config failed: %v", io.EOF) —— 此时 errors.Is(err, io.EOF) 返回 false
%w 不合法:fmt.Errorf("a: %w, b: %w", err1, err2) 编译报错Go 标准库不提供“最大包装层数”开关,但你可以用封装函数主动截断。常见场景是中间件或通用工具函数反复包装同一错误(如重试、日志装饰),导致链过长甚至循环引用。
errors.Unwrap 手动取最内层原始错误:original := errors.Unwrap(errors.Unwrap(err)) // 最多两层
func flattenErr(err error) error {
for errors.Unwrap(err) != nil {
if unwrapped := errors.Unwrap(err); unwrapped != nil {
err = unwrapped
} else {
break
}
}
return err
}Unwrap() 方法并返回自身(如某些第三方库 bug),可能造成死循环,建议加计数限制fmt.Printf("%+v", err) 是最直接的方式——它会打印所有包装层级及各层堆栈(前提是错误值实现了 fmt.Formatter,如 errors.Join 或 github.com/pkg/errors 的旧版)。标准 fmt.Errorf 包装的错误默认不带堆栈,所以 %+v 只显示文本链,不显示行号。
fmt.Errorf("%w", err) + 第三方库(如 github.com/cockroachdb/errors),或自己实现 Unwrap() error 和 Format() 方法var seen map[error]bool
func isCyclic(err error) bool {
if seen == nil {
seen = make(map[error]bool)
}
if seen[err] {
return true
}
seen[err] = true
if u := errors.Unwrap(err); u != nil {
return isCyclic(u)
}
return false
}错误链本身轻量,但
滥用 %w 在高并发或深调用栈中容易掩盖真正根因,尤其当每层都加相似前缀(如 “handler: service: db:”)时,读起来反而更费劲。关键不是层数多少,而是每层是否带来新上下文。