Go 的错误处理:包一层、判一下、记一次

举报
yd_271078175 发表于 2026/11/09 08:32:37 2026/11/09
【摘要】 Go 的错误处理常被吐槽啰嗦,但真正让人踩坑的不是 if err != nil,而是三个更细的问题:什么时候该包、怎么判断错误类型、日志该在哪一层打。这三件事做错,代码能跑,排查时会非常难受。

Go 的错误处理常被吐槽啰嗦,但真正让人踩坑的不是 if err != nil,而是三个更细的问题:什么时候该包、怎么判断错误类型、日志该在哪一层打。这三件事做错,代码能跑,排查时会非常难受。

一、包错误用 %w,不要用 %v

给错误加上下文是好事,但方式有讲究:

// 丢了原始错误,调用方再也判断不了
return fmt.Errorf("读取配置失败: %v", err)

// 保留了错误链
return fmt.Errorf("读取配置失败: %w", err)

%v 只是把错误转成字符串拼进去,原始错误对象就没了。调用方拿到的是一个全新的 error,errors.Is 一律返回 false。

%w 会把原错误包在新错误里,形成一条链。调用方可以一路 errors.Is(err, fs.ErrNotExist) 判断到底是不是文件不存在,而不需要匹配字符串。

只在一种情况下用 %v:你确定这个错误不需要被上层识别,只是想留个现场。

二、判断错误用 errors.Is,不要比字符串

// 脆弱:文案一改就失效
if err.Error() == "file not found" { ... }

// 正确
if errors.Is(err, fs.ErrNotExist) { ... }

字符串判断的问题在跨层时特别明显:底层把 file not found 改成 no such file,上层所有判断同时失效,而且不会报错,只是行为悄悄变了。

需要从错误里取信息时用 errors.As:

var pe *fs.PathError
if errors.As(err, &pe) {
    fmt.Println("出问题的路径:", pe.Path)
}

errors.As 会沿着错误链找第一个匹配的类型,再把值赋给目标变量。比自己逐层做类型断言省事得多。

三、定义哨兵错误,不要到处 new

需要让调用方区分「哪一类错误」时,在包里定义好变量:

var (
    ErrNotLoggedIn = errors.New("未登录")
    ErrRateLimited = errors.New("被限流")
)

返回时直接 return ErrRateLimited 或者包装一下 fmt.Errorf("第 %d 次请求: %w", i, ErrRateLimited),调用方统一用 errors.Is 判断。

这样错误从字符串变成了类型系统的一部分:改名、改文案都不影响判断。

四、日志只记一次,在哪一层记要想清楚

一个常见的坏味道是同一个错误在日志里出现三次:

func a() error {
    if err := b(); err != nil {
        log.Printf("a 失败: %v", err)
        return err
    }
    return nil
}

底层记、中间层记、最外层再记,排查时看到的像是三个不同的故障。

规矩是:谁处理这个错误,谁记录它。中间层只负责加上下文往回传:

if err := b(); err != nil {
    return fmt.Errorf("同步任务 %s: %w", id, err)
}

只有最终决定「这次操作失败了,要让人知道」的那一层才打日志。这样一条日志就是一次失败的完整结论,前面的上下文都在错误链里。

五、不要在两处同时处理和返回

最后一条边界情况:有人习惯「记完日志再返回错误」,结果上层又记一次。判断标准很简单——如果这一层不打算让程序停下来,就别记 Error,传上去就好。

反过来,如果这一层要吞掉错误(比如批量任务里某一条失败不影响其余),那就必须记,而且要带够信息:哪一条、为什么、后续怎么处理。

for _, item := range items {
    if err := process(item); err != nil {
        // 吞掉但记录,带上 item 的标识
        slog.Warn("处理失败,跳过", "item", item.ID, "err", err)
        continue
    }
}

这种情况下用 Warn 比 Error 更准确:整体任务是成功的,只是有一条没做。

六、小结

四句话:

1. 包错误用 %w,保住错误链; 2. 判断用 errors.Is / errors.As,别比字符串; 3. 可识别的错误定义成哨兵变量; 4. 错误只在处理它的那一层记录。

这些都不影响程序跑起来,影响的是三个月后你在日志里找问题的时间。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。