编程笔记

lifelong learning & practice makes perfect

译|Go 不再修改错误处理的语法

关于 Go 的最古老且最持久的抱怨之一是错误处理的冗长性。我们都非常熟悉(有些人可能会说是痛苦地)这种代码模式:

1
2
3
4
x, err := call()
if err != nil {
// handle err
}

if err != nil 的测试可能非常普遍,以至于淹没了代码的其余部分。这种情况通常发生在执行大量 API 调用的程序中,并且错误处理是基本的,它们只是被返回。有些程序最终会得到如下所示的代码:

1
2
3
4
5
6
7
8
9
10
11
12
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err != nil {
return err
}
y, err := strconv.Atoi(b)
if err != nil {
return err
}
fmt.Println("result:", x + y)
return nil
}

在这个函数体中的十行代码中,只有四行(调用和最后两行)似乎在做实际的工作。其余六行看起来像是噪音。这种冗长性是真实存在的,因此,多年来,对错误处理的抱怨一直位居年度用户调查的首位也就不足为奇了。(有一段时间,缺少泛型超过了对错误处理的抱怨,但现在 Go 支持泛型,错误处理又回到了首位。)

Go 团队认真对待社区的反馈,因此多年来,我们一直尝试与 Go 社区的投入一起,为这个问题提出解决方案。

Go 团队的第一次明确尝试可以追溯到 2018 年,当时 Russ Cox 作为当时我们称之为 Go 2 工作的一部分,正式描述了这个问题。他概述了一个基于 Marcel van Lohuizen 的 草案设计 的可能解决方案。该设计基于 checkhandle 机制,并且相当全面。该草案包括对替代解决方案的详细分析,包括与其他语言采用的方法的比较。如果您想知道您特定的错误处理想法是否以前被考虑过,请阅读本文档!

1
2
3
4
5
6
7
8
// 使用提议的 check/handle 机制实现的 printSum。
func printSum(a, b string) error {
handle err { return err }
x := check strconv.Atoi(a)
y := check strconv.Atoi(b)
fmt.Println("result:", x + y)
return nil
}

checkhandle 方法被认为过于复杂,大约一年后,在 2019 年,我们跟进了更简化且现在臭名昭著的try 提案。它基于 checkhandle 的思想,但 check 伪关键字变成了 try 内置函数,并且省略了 handle 部分。为了探索 try 内置函数的影响,我们编写了一个简单的工具 (tryhard),该工具使用 try 重写现有的错误处理代码。该提案经过了激烈的争论,在 GitHub 问题 上接近 900 条评论。

1
2
3
4
5
6
7
8
// 使用提议的 try 机制实现的 printSum。
func printSum(a, b string) error {
// 使用 defer 语句在返回之前增强错误
x := try(strconv.Atoi(a))
y := try(strconv.Atoi(b))
fmt.Println("result:", x + y)
return nil
}

然而,try 通过在发生错误时从封闭函数返回来影响控制流,并且这样做是从可能深度嵌套的表达式中返回,从而隐藏了这种控制流。这使得该提案对许多人来说是不可接受的,尽管对该提案进行了大量投资,但我们决定放弃这项努力。回想起来,引入一个新关键字可能会更好,因为我们现在可以通过 go.mod 文件和特定于文件的指令来精细控制语言版本。将 try 的使用限制在赋值和语句中可能会减轻其他一些担忧。Jimmy Frasche 最近提出的 提案 本质上回到了最初的 checkhandle 设计,并解决了该设计的一些缺点,正在朝着这个方向发展。

try 提案的影响导致了大量的反思,包括 Russ Cox 的一系列博客文章:“思考 Go 提案过程”。一个结论是,我们可能通过提出一个几乎完全成熟的提案,几乎没有社区反馈的空间和一个“具有威胁性”的实施时间表,从而降低了我们获得更好结果的机会。根据“Go 提案过程:重大变更”: “回想起来,try 是一个足够大的变更,我们发布的新设计 […] 应该是第二个草案设计,而不是带有实施时间表的提案”。但无论在这种情况下可能存在过程和沟通失败,用户对该提案的情绪都非常强烈地不支持。

当时我们没有更好的解决方案,并且几年没有进行错误处理的语法更改。不过,社区中的很多人受到了启发,我们收到了源源不断的错误处理提案,其中许多提案彼此非常相似,有些很有趣,有些难以理解,有些不可行。为了跟踪不断扩展的格局,一年后,Ian Lance Taylor 创建了一个 总括问题,其中总结了当前改进错误处理的拟议更改的状态。创建了一个 Go Wiki 来收集相关的反馈、讨论和文章。独立地,其他人已经开始跟踪多年来的所有错误处理提案。看到所有这些提案的数量之多令人惊叹,例如在 Sean K. H. Liao 的博客文章“go 错误处理提案”中。

对错误处理冗长性的抱怨持续存在(参见 Go 开发者调查 2024 H1 结果),因此,经过一系列日益完善的 Go 团队内部提案后,Ian Lance Taylor 在 2024 年发布了“使用 ? 减少错误处理样板代码”。这次的想法是从 Rust 中实现的构造中借鉴,特别是 ? 运算符。希望通过依靠使用已建立符号的现有机制,并考虑到我们多年来学到的东西,我们应该能够最终取得一些进展。在向程序员展示使用 ? 的 Go 代码的小型非正式用户研究中,绝大多数参与者正确地猜测了代码的含义,这进一步说服我们再次尝试。为了能够看到更改的影响,Ian 编写了一个工具,将普通的 Go 代码转换为使用提议的新语法的代码,并且我们还在编译器中原型化了该功能。

1
2
3
4
5
6
7
// 使用提议的 "?" 语句实现的 printSum。
func printSum(a, b string) error {
x := strconv.Atoi(a) ?
y := strconv.Atoi(b) ?
fmt.Println("result:", x + y)
return nil
}

不幸的是,与其他错误处理想法一样,这个新提案也很快被评论和许多针对细微调整的建议所淹没,这些建议通常基于个人偏好。Ian 关闭了该提案,并将内容移至 讨论 以促进对话并收集更多反馈。略作修改的版本收到了 稍微积极的评价,但广泛的支持仍然难以捉摸。

经过这么多年的尝试,Go 团队提出了三个完整的提案,以及来自社区的字面上的 数百个! 提案,其中大多数是主题的变体,但所有这些提案都未能获得足够(更不用说压倒性的)支持,我们现在面临的问题是:如何进行?我们应该继续进行吗?

我们认为不应该。

更准确地说,至少在可预见的将来,我们会停止尝试解决语法问题提案流程 为此决定提供了理由:

提案流程的目标是在合理的时间内就结果达成普遍共识。如果在问题跟踪器上的问题讨论中无法确定普遍共识,通常的结果是该提案被拒绝。

此外:

可能发生提案审查无法确定普遍共识,但很明显该提案不应被彻底拒绝。[…] 如果提案审查小组无法确定共识或提案的下一步,则有关前进道路的决定将传递给 Go 架构师[…],他们审查讨论并旨在他们之间达成共识。

没有任何错误处理提案达到接近共识的程度,因此它们都被拒绝了。即使是 Google 的 Go 团队中最资深的成员,目前也没有就最佳前进道路达成一致意见(也许在某个时候会发生变化)。但如果没有强烈的共识,我们就无法合理地前进。

有支持现状的有效论点:

  • 如果 Go 早些时候引入了用于错误处理的特定语法糖,那么今天很少有人会争论它。但我们已经走了 15 年,机会已经过去,Go 有一种非常好的处理错误的方式,即使它有时可能看起来很冗长。

  • 从不同的角度来看,让我们假设我们今天遇到了完美的解决方案。将其纳入语言只会导致一群不高兴的用户(支持更改的用户)变成另一群不高兴的用户(喜欢现状的用户)。当我们决定向语言添加泛型时,我们处于类似的情况,尽管有一个重要的区别:今天没有人被迫使用泛型,并且编写良好的泛型库使用户可以主要忽略它们是泛型的事实,这要归功于类型推断。相反,如果向语言添加了用于错误处理的新语法结构,实际上每个人都需要开始使用它,否则他们的代码将变得不地道。

  • 不添加额外的语法符合 Go 的设计规则之一:不要提供多种做同一件事的方式。在“人流量”大的领域,此规则存在例外:赋值就是其中之一。具有讽刺意味的是,在 短变量声明 (:=) 中重新声明变量的能力是为了解决因错误处理而出现的问题而引入的:如果没有重新声明,则每次检查的错误检查序列都需要一个不同命名的 err 变量(或额外的单独变量声明)。当时,一个更好的解决方案可能是为错误处理提供更多的语法支持。然后,可能不需要重新声明规则,并且随之而来的各种相关 复杂性 也会消失。

  • 回到实际的错误处理代码,如果错误实际上被处理,那么冗长性就会消失在背景中。良好的错误处理通常需要添加到错误中的其他信息。例如,用户调查中反复出现的评论是关于与错误关联的缺少堆栈跟踪。这可以通过生成和返回增强错误的支持函数来解决。在这个(公认是人为的)示例中,样板代码的相对数量要小得多:

1
2
3
4
5
6
7
8
9
10
11
12
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err != nil {
return fmt.Errorf("invalid integer: %q", a)
}
y, err := strconv.Atoi(b)
if err != nil {
return fmt.Errorf("invalid integer: %q", b)
}
fmt.Println("result:", x + y)
return nil
}
  • 新的标准库功能也可以帮助减少错误处理样板代码,这与 Rob Pike 2015 年的博客文章“错误是值”的精神非常一致。例如,在某些情况下,可以使用 cmp.Or 一次性处理一系列错误:
1
2
3
4
5
6
7
8
9
func printSum(a, b string) error {
x, err1 := strconv.Atoi(a)
y, err2 := strconv.Atoi(b)
if err := cmp.Or(err1, err2); err != nil {
return err
}
fmt.Println("result:", x+y)
return nil
}
  • 编写、阅读和调试代码都是非常不同的活动。编写重复的错误检查可能很乏味,但今天的 IDE 提供了强大的,甚至是 LLM 辅助的代码自动补全功能。对于这些工具来说,编写基本的错误检查是直截了当的。冗长性在阅读代码时最为明显,但工具也可以在这里提供帮助;例如,具有 Go 语言设置的 IDE 可以提供一个切换开关来隐藏错误处理代码。对于其他代码段(如函数体),已经存在这样的开关。

  • 在调试错误处理代码时,能够快速添加 println 或拥有一个专用的行或源位置来在调试器中设置断点是有帮助的。当已经有一个专用的 if 语句时,这很容易。但是,如果所有错误处理逻辑都隐藏在 checktry? 后面,则可能必须首先将代码更改为普通的 if 语句,这会使调试复杂化,甚至可能引入微妙的错误。

  • 还有一些实际的考虑因素:为错误处理提出一个新的语法想法是很便宜的;因此,社区中出现了大量的提案。但要想出一个经得起推敲的好方案就不那么容易了。这需要共同努力来正确设计语言更改并实际实施它。真正的成本仍然在之后:所有需要更改的代码,需要更新的文档,需要调整的工具。考虑到所有这些,语言更改非常昂贵,Go 团队相对较小,并且有很多其他优先事项需要解决。(后几点可能会改变:优先事项可能会转移,团队规模可能会增加或减少。)

  • 最后,我们中的一些人最近有机会参加 Google Cloud Next 2025,Go 团队在那里设有一个展位,并且我们还举办了一个小型 Go Meetup。我们有机会询问的每一位 Go 用户都坚决认为我们不应该更改语言以实现更好的错误处理。许多人提到,当刚从另一种支持该功能的语言过来时,Go 中缺少特定的错误处理支持最为明显。随着一个人变得更加流利并编写更多地道的 Go 代码,这个问题变得不那么重要了。当然,这并不是一个足够大的人群来代表,但它可能与我们在 GitHub 上看到的人群不同,并且他们的反馈是另一个数据点。

当然,也有支持变革的有效论点:

  • 缺乏更好的错误处理支持仍然是我们用户调查中的首要抱怨。如果 Go 团队真的认真对待用户反馈,我们最终应该对此做些什么。(尽管似乎也没有 对语言更改的压倒性支持。)

  • 也许对减少字符数的单一关注是错误的。一个更好的方法可能是使用关键字使默认错误处理高度可见,同时仍然删除样板代码 (err != nil)。这种方法可能使读者(代码审查员!)更容易看到错误已被处理,而无需“仔细查看”,从而提高代码质量和安全性。这将使我们回到 checkhandle 的开端。

  • 我们并不真正知道问题的关键在于错误检查的直接语法冗长性,还是良好错误处理的冗长性:构建对 API 有用且对开发人员和最终用户都有意义的错误。这是我们希望更深入研究的事情。

然而,迄今为止,解决错误处理问题的任何尝试都没有获得足够的支持。如果我们诚实地评估一下我们的现状,我们只能承认,我们既没有对问题达成共识,也没有一致认为首先存在问题。有鉴于此,我们做出如下务实的决定:

在可预见的将来,Go 团队将停止寻求用于错误处理的语法语言更改。我们还将关闭所有开放的和传入的,主要关注错误处理语法的提案,而无需进一步调查。

社区为探索、讨论和辩论这些问题付出了巨大的努力。虽然这可能没有导致对错误处理语法的任何更改,但这些努力导致了 Go 语言和我们流程的许多其他改进。也许,在未来的某个时刻,关于错误处理的更清晰的画面将会出现。在那之前,我们期待着将这种令人难以置信的热情集中在新机会上,使 Go 对每个人都更好。

谢谢!

欢迎关注我的其它发布渠道

Related Issues not found

Please contact @yiGmMk to initialize the comment