如何使用Golang包装错误信息_使用fmt.Errorf和%w进行包装

错误包装是将原始错误嵌入新错误并附加语义描述的机制,Go 1.13 起 fmt.Errorf 支持 %w 动词实现可展开、可检查的包装,%w 仅接受一个 error 类型参数且须位于格式字符串末尾。

在 Go 中,错误包装(error wrapping)是将底层错误与更高层上下文信息结合的关键机制,便于调试和日志追踪。从 Go 1.13 开始,fmt.Errorf 支持 %w 动词,可创建可展开、可检查的包装错误。

什么是错误包装?

错误包装指将一个原始错误(如 I/O 失败、空指针)嵌入到一个新的错误中,并附加语义描述(如“读取配置文件失败”)。被包装的错误仍可被程序识别和处理,不会丢失原始原因。

关键点:

  • %w 只接受一个 error 类型参数,且仅能出现在格式字符串末尾(或唯一占位符)
  • %w 包装后的错误实现了 Unwrap() error 方法,支持 errors.Iserrors.As
  • %v 或 %s 打印被包装的错误——它们只显示顶层消息,不递归展开

正确使用 fmt.Errorf + %w

包装时应保持语义清晰,避免冗余,同时保留原始错误的可检查性。

示例:

func readFile(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("failed to read config file %q: %w", path, err)
    }
    return data, nil
}

这里 %wos.ReadFile 返回的底层错误(如 os.ErrNotExist)完整包裹,上层调用者仍可用 errors.Is(err, os.ErrNotExist) 判断。

注意事项:

  • %w,否则编译报错
  • fmt.Errorf("retry: %w", fmt.Errorf("fail: %w", err))),会破坏错误链结构

如何检查和解包包装错误

使用标准库函数安全地识别和提取原始错误。

判断是否为某类错误:

if errors.Is(err, os.ErrNotExist) {
    log.Println("config file missing")
}

提取具体错误类型:

var pathErr *os.PathError
if errors.As(err, &pathErr) {
    log.Printf("file access failed on %s", pathErr.Path)
}

手动解包(较少用):

cause := errors.Unwrap(err) // 返回被 %w 包裹的直接下层 error
for cause != nil {
    log.Printf("wrapped: %+v", cause)
    cause = errors.Unwrap(cause)
}

常见误区与替代方案

避免以下写法:

  • fmt.Errorf("failed: %v", err) —— 丢失可检查性,变成纯字符串
  • fmt.Errorf("failed: %w, retrying...", err) —— %w 必须是最后一个动词
  • log.Printf("%v", err) —— 推荐用 %+v 查看完整错误链(需第三方库如 github.com/pkg/errors 或 Go 1.20+ 的内置增强)

如果需要更丰富的错误元数据(如时间戳、调用栈),可考虑封装自定义错误类型并实现 Unwrap()Error(),但多数场景 fmt.Errorf + %w 已足够。