Go 错误处理的演化和最新提案

简化错误处理:可能吗?

image.png|300

注:本文核心内容由大语言模型生成,辅以人工事实核查与结构调整。

Go 的错误处理机制,尤其是在 Go 1.13 引入 errors.Iserrors.As 之后,旨在提供一种结构化且可追踪的方法。然而,尽管这是经过深思熟虑的设计,它依然是 Go 语言中 最具争议的话题之一。在泛型加入之后的各类调查中,它经常被提及为最常见的问题。这场持续的讨论凸显了 Go 社区在“改进愿望”与“坚持核心哲学”之间的紧张关系。


当前面临的挑战

Go 错误处理的主要问题,也是开发者最常抱怨的地方,就是它的 冗长与重复样板代码。模式 if err != nil { return err } 无处不在,在代码库里“数不清地”重复出现,GitHub 搜索结果也能证明这一点。

这种冗长带来了多个问题:

  • 可读性降低:很多开发者认为,大量的错误检查掩盖了函数的主要逻辑,使得“更难看清方法到底在做什么”。有些人形容阅读这些重复代码就像是一种“负担”。

  • errors.As 不够符合人体工学:目前的 errors.As 用法要求开发者必须预先声明一个目标错误类型的变量,然后传入它的指针。这被认为是“不符合人体工学”的——样板化、导致变量作用域泄漏,并且很像 C 语言那种“输出参数”,而不是 Go 一贯提倡的多返回值风格。

    当前的 errors.As 用法:

    1
    2
    3
    4
    5
    6
    7
    8
    err := foo()
    if err != nil {
    var myErr *MyCustomError
    if errors.As(err, &myErr) { // myErr 的作用域会超出此代码块
    // 处理 myErr
    }
    // ...
    }
  • 开发者挫败感:社区里有些人觉得错误处理改进一直没有实质进展,导致一种“永远僵局”的苦涩感。显式处理常常被视为“繁琐笨拙”。

    常见的样板代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    err := doSomethingTowardMyActualGoal()
    if err != nil {
    return err
    }
    err = doTheNextInterestingThing()
    if err != nil {
    return err
    }
    // ... 实际逻辑 ...
  • 缺乏堆栈追踪:一些用户还认为,缺少堆栈追踪功能对调试来说是个很大的痛点。


支持当前做法的论点

尽管存在上述问题,许多 Go 开发者和语言设计者依然坚决捍卫当前的错误处理哲学,并提出以下优势:

  • 显式性与责任链:Go 的方式迫使开发者显式地处理错误,形成一种“责任传递的流程”。这带来“安心感”,并防止错误被静默忽略,或像异常那样导致意料之外的控制流问题。

  • 清晰性与可排错性:显式错误检查让程序“超级清晰地展现出可能在哪儿出错”,这有助于排查问题。它尤其在处理“边缘场景”时有帮助——这些地方往往比“主路径”更关键、更复杂。

  • 有意为之的设计:当前系统是一个“非常有意图的语言设计”,根本上基于这样一种信念:错误值比异常提供了更好的语义。

  • 简单性:系统足够简单,初学者也能轻松掌握,同时又为复杂错误处理提供了必要的构建模块,符合“保持简单(Keep It Simple)”的哲学。

  • 避免异常的陷阱:异常会隐藏控制流、展开调用栈、丢失上下文,并且在很多语言中经常被忽略。Go 的方法正是为了避免这些问题。

  • 不是 DRY 原则的违反:有些人认为,反复写 if err != nil { return err } 并不是违反了“不要重复自己(DRY)”原则,因为每一个错误检查,虽然语法类似,但语义上针对的是“完全不同的事情”。

  • 高级功能:Go 已经引入了 errors.Join(),同时 errors.Iserrors.As 也被认为为复杂错误处理提供了显著优势,是其他语言所不具备的。

提议的改进与替代方案

Go 社区提出了许多改进错误处理的方法,其中不少灵感来自其他语言:

  • 错误传播的语法糖:最常见的诉求是简化样板化的 if err != nil { return err }

    • Rust 的 ? 运算符:经常被引用为理想方案,用于简洁地传播错误。Go 的等价提案可能是 value, ? := myfunc(…),其中 ? 会自动处理错误的返回。

    • 单行条件语句:允许 if err != nil return err,以减少代码的垂直空间占用。

    • 新增运算符:例如在函数调用后加上 !,表示“如果错误不为 nil 就返回”。

  • 基于泛型的 errors.As 改进(IsA/AsA):一个重要提案(Issue #51945)建议引入新的类型参数化函数 AsA,替代现有的 errors.As 模式。

    • 优势

      • 提供更符合人体工学的“逗号 ok”模式(Go 开发者熟悉的风格);

      • 将错误变量的作用域限制在必要范围;

      • 提供 编译期类型安全(避免因错误的 target 类型导致运行时 panic);

      • 显著的性能提升(快 50–70%),通过避免反射和减少堆分配实现。

    • 提议的 errors.AsA 用法:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      err := someOperation()
      if err != nil {
      if myErr, ok := errors.AsA[*MyCustomError](err); ok { // myErr 的作用域仅在此代码块
      // 处理 myErr
      } else if otherErr, ok := errors.AsA[*OtherError](err); ok {
      // 处理 otherErr
      }
      // 处理其他情况
      }
    • 挑战AsA 的返回签名 (_ E, ok bool) 并不能像当前 errors.As 那样优雅地融入 switch 语句。不过,这一提案已获得关注,并进入“活跃(Active)”评审阶段。

  • try 关键字:受 Zig 等语言启发,有些提案建议为 Go 2 引入 try 关键字。

    • 更复杂的 try 提案包括隐式跳转到当前函数中的 catch: 标签,以便进行错误注解或恢复,可能还会配合新的 errors.Annotate() 函数。

    • 但也有人反对 try 直接导致立即 return 的行为,因为这可能会削弱错误注解的使用。


常见误解/被驳斥的观点

在争论过程中,关于 Go 错误处理的一些观点被挑战或驳斥:

  • “样板代码违反 DRY 原则”:尽管这些代码片段看起来相似,但每个 if err != nil { return err } 实际上都是针对“不同操作”的检查,因此更像是编程结构的重复使用,而非真正的 DRY 违背。

  • “你不一定要用它”作为新特性的理由:这一说法基本被否定,因为一旦语言新增特性,就会“污染”开源代码和团队代码,迫使所有开发者不得不接触。

  • “Go 社区曾经反对泛型”:虽然确实有个别人反对泛型,但更广泛的观点是希望找到一个符合 Go 设计哲学的解决方案,而最终这也确实实现了。

  • “快乐路径(happy path)可读性最重要”:Go 的支持者认为,让“快乐路径”过于简洁反而是“聪明代码陷阱”。Go 更注重“边缘路径和错误分支”的可读性,因为这些地方往往更复杂、更关键,也更利于调试。

  • “Go 的错误处理比其他语言更啰嗦”:有人指出,虽然 Go 的显式检查看似冗长,但在 Java 等语言中,正确的错误处理(包括 throws 声明、try 块和 finally 子句)通常会产生更多代码行,而且依然可能导致未处理的异常。

  • 无大括号的条件语句:比如 if err != nil return err 这样的简写虽然能减少冗余,但有人反对,认为无大括号条件语句存在安全隐患,并增加语法复杂性。

用户体验

Go 的错误处理在用户体验上具有很强的主观性和差异性:

  • 积极反馈:许多开发者表示满意,认为它“简单直接”、“线性”、“可靠”,并且“易于阅读和理解”。显式的错误处理带来了“安心感”,并能快速定位潜在的出错点。

  • 新手挑战:对于 Go 新手而言,错误处理经常被视为一个“难点”,尤其是从使用异常机制的语言转过来时。

  • 争议性:这一问题依然高度争议,双方立场鲜明。一些用户觉得 Go 社区在抗拒改变上过于教条。

  • 工具辅助:像 GitHub Copilot 这样的工具被认为能减轻输入样板代码的负担。

  • 对代码逻辑的影响:对某些人来说,冗长的错误检查“妨碍了对代码核心逻辑的理解”。

  • 第三方库的问题:当底层库返回未导出的错误类型时,可能会遇到困难,迫使开发者退而求其次,使用基于字符串的错误检查方式。

那句“Gofmt 的风格没有人最喜欢,但 gofmt 却是所有人最喜欢的”的谚语,有时也被套用于 Go 的错误处理,意味着虽然没人完全爱它,但它确实为社区发挥了积极作用。最终,许多开发者接受了一个现实:没有任何语言能完全满足所有人的期望,而他们依然能够欣赏 Go 本身的价值。

参考

更多内容

最近文章:

随机文章:


更多该系列文章,参考medium链接:

https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a

English post: https://programmerscareer.com/go-errors-asa/
作者:微信公众号,Medium,LinkedIn,Twitter
发表日期:原文在 2025-09-10 11:19 时创作于 https://programmerscareer.com/zh-cn/go-errors-asa/
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

包管理器和依赖项的责任 Go指针:最佳实践和新的初始化提议

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×