请点击上方蓝字TonyBai订阅公众号!
错误处理一直是编程中的重要组成部分,Go语言以其独特的错误处理机制而闻名。Go摒弃了传统主流语言(如Java等)的异常处理模型,而是将错误作为函数返回值的一部分,鼓励开发者显式地处理每个可能出现的错误。这种机制的核心在于error接口和if err != nil语句。然而,随着Go项目规模的增长,if err != nil的频繁出现导致了大量的代码冗余,让代码显得不那么优雅。因此,如何改进错误处理机制一直是Go社区讨论的热点话题。
近期,Go团队核心开发者Ian Taylor[1]提出了一项新的提案“proposal: spec: reduce error handling boilerplate using ?”[2],旨在通过引入?操作符来简化错误处理流程。该提案建议允许开发者使用?来“吸收”函数返回的错误值,并在错误不为nil时执行特定的代码块或提前返回。这一提案迅速引发了社区的广泛关注和讨论。
在这篇文章中,我就基于issue内容简单为大家介绍一下该提案的核心内容、优缺点以及Go社区的反馈意见情况。
1. 提案的核心内容
Ian Taylor提案的核心是引入一个新的操作符?,它可以放在返回error类型值的表达式后面。其基本用法如下:
// 传统写法
r, err := SomeFunction()
if err != nil {
return fmt.Errorf("something failed: %v", err)
}
// 使用?操作符的写法
r := SomeFunction() ? {
return fmt.Errorf("something failed: %v", err)
}
在这个例子中,SomeFunction()预期返回一个值(r)和一个错误。新写法中的?操作符会检查返回的错误值,如果错误不为nil,则执行后面的代码块。在这个代码块中,err变量被隐式声明,并绑定到返回的错误值。
?操作符还支持省略代码块的用法,在这种情况下,如果错误不为nil,则函数会立即返回该错误:
// 使用?操作符的写法
SomeFunction2() ?
这等同于:
// 传统写法
if err := SomeFunction2(); err != nil {
return err
}
2. 正式提案中的?语法规则
在正式提案中,Ian Taylor详细阐述了?的语法规则:
?可以出现在赋值语句或表达式语句的末尾。
对于赋值语句,?操作符“吸收”的是右侧表达式的最后一个值,并且右侧值的数量必须比左侧变量的数量多一个。
对于表达式语句,?操作符“吸收”的是表达式的最后一个值,并且表达式不能没有返回值。
被“吸收”的值(称为qvalue)必须是实现了error接口的接口类型,通常它就是类型error。
?后面可以选择性地跟一个代码块。如果没有代码块,并且函数有返回值且最后一个返回值的类型实现了error接口,那么当qvalue不为nil时,函数会立即返回,并将qvalue赋给最后一个返回值。
如果?后面有代码块,并且qvalue不为nil,那么代码块会被执行。在代码块中,会隐式声明一个名为err的变量,其值和类型与qvalue相同。
3. 提案的优势分析
Ian Taylor在提案中给出了几点优势:
代码简化
?操作符最显著的优势在于它可以显著减少错误处理的代码量。例如,将:
r, err := SomeFunction()
if err != nil {
return fmt.Errorf("something failed: %v", err)
}
改写为:
r := SomeFunction() ? {
return fmt.Errorf("something failed: %v", err)
}
可以将错误处理的样板代码从9个token减少到5个,非空白字符从24个减少到12个,样板代码行数从3行减少到2行。
在比如将:
r, err := SomeFunction()
if err != nil {
return err
}
改写为:
r := SomeFunction() ?
可以将样板代码从9个token减少到1个,非空白字符从24个减少到 1 个,样板代码行数从3行减少到0行。
可读性提升
?操作符的另一个重要优势是它可以使代码的主流程更加清晰。通过将错误处理逻辑放在?后面的代码块中,可以避免if err != nil语句对代码主流程的干扰,使代码的逻辑更加直观。
控制流明确
与之前的try提案不同,?操作符不会引入隐藏的控制流。?的存在明确地指示了错误处理的逻辑,虽然这个操作符很小,但前面的空格使其更加显眼。
错误处理模式统一
目前,Go中存在多种错误处理模式,例如:
var (
v string
err error
)
if v, err = F(); err != nil {
...
}
和
v, err := F()
if err != nil {
...
}
?
操作符可以统一这些模式,鼓励开发者使用:
v := F() ? {
...
}
标准库应用实例
Ian Taylor还通过一个简单的重写工具对标准库进行了修改,展示了?在实际代码中的应用。例如,在archive/tar/writer_test.go中:
// Test that we can get a long name back out of the archive.
reader := NewReader(&buf)
hdr = reader.Next() ? {
t.Fatal(err)
}
if hdr.Name != longName {
...
4. 提案的不足与争议
相对于优点,Ian Taylor列出的该方案的不足数量更多!我们逐个看一下:
与现有语言的差异
尽管?操作符在某种程度上借鉴了Rust的?操作符,但它们之间仍然存在差异。例如,Rust允许?出现在表达式的中间,而Go提案只允许?出现在语句的末尾。这种差异可能会给熟悉Rust的Go新手带来一些困惑。
err变量的隐式声明
?后面的代码块会隐式声明一个err变量,这可能会导致变量shadowing的问题。例如,在以下代码中:
for n = 1; !utf8.FullRune(r.buf[:n]); n++ {
r.buf[n], err = r.readByte()
if err != nil {
if err == io.EOF {
err = nil // must change outer err
break
}
return
}
}
// code that later returns err
err = nil语句必须修改外层的err变量,而使用?会引入一个新的err变量,导致编译错误。
社区中也有人建议显式声明err变量,例如:
r := SomeFunction() ? err {
return fmt.Errorf("something failed: %v", err)
}
但Ian Taylor认为这会导致额外的样板代码,违背了提案的初衷。
可选代码块的争议
?后面的代码块是可选的,这在Go中是独一无二的。这会导致一些问题,例如插入或删除换行符可能会改变代码的含义:
func F1() error {
err := G1()
log.Print(err)
G2() ?
{
log.Print(err)
}
return nil
}
func F2() error {
err := G1()
log.Print(err)
G2() ? {
log.Print(err)
}
return nil
}
这两个函数都合法,且只有换行符的差异,但它们的行为却完全不同。
?的遗漏风险
对于调用只有一个返回错误值的函数,开发者可能会忘记添加?,例如写成F()而不是F() ?。虽然目前也存在忘记检查错误的问题,但在习惯了?之后,阅读代码时可能会更容易忽略这个错误。
错误注解的减少
?的简便性可能会鼓励开发者直接返回错误,而不是添加注解,例如使用?而不是 ? { return fmt.Errorf("... %v", err) }。这可能会导致错误信息的丢失。
一次性机会的压力
Go社区对错误处理机制的改变非常谨慎,因为这可能会影响大量的现有代码。因此,只有一次试错机会,社区需要仔细考虑这个提案是否是最佳方案。
不改变的合理性
尽管Go的错误处理机制经常受到批评,但它仍然是可用的。因此,社区需要权衡是否真的需要进行改变。Ian Taylor在提案中反复提到:"Perhaps no change is better than this change. Perhaps no change is better than any change"。这也一定程度上反映出Go团队在错误处理改进方面其实并不那么坚定,感觉更多是迫于Go社区的舆论和压力。
5. 社区反馈与讨论
对在表达式中使用?的讨论
kortschak提出了在OtherFunction(SomeFunction() ?)和for _, v := range (SomeFunction() ?) { … }这样的语句中使用?的可能性。Ian Taylor回应说,?只允许出现在语句的末尾,不允许出现在表达式的中间,因为这会导致控制流的混乱,这也是try提案存在的问题之一。
对简化程度的质疑
Merovius 认为,带有代码块的?版本(第一版)的简化程度不够明显,例如x, err := f(); if err != nil { … }和x := f() ? { … } 看起来差别不大。他建议只保留没有代码块的版本(第二版)。
对扩展到fmt.Errorf的建议
gaal提出了一个扩展建议,允许?后面直接跟fmt.Errorf的参数,例如:
data := symtabSection.Data() ? "cannot load symbol section: %w", err
这可以进一步简化错误处理代码。
赋值顺序和临时变量的问题
jrick提出了关于赋值顺序和临时变量的问题,例如:
var x *int
*x = foo() ? // returns the error
// not reached
这段代码是否会因为?提前返回而避免nil dereference错误?Ian Taylor 解释说,当?后面有代码块时,赋值语句会在代码块执行前完成;当没有代码块时,赋值语句不会执行。
6. 小结
Ian Taylor提出的?操作符提案为Go语言的错误处理机制提供了一种新的思路。该提案通过引入简洁的语法,可以显著减少错误处理的代码量,并使代码的主流程更加清晰。然而,该提案也存在一些潜在的问题,例如与现有语言的差异、err变量的隐式声明、可选代码块带来的复杂性等。
该提案在Go社区引发了广泛的讨论,社区成员从不同的角度对该提案进行了分析和评价,提出了一些有趣的建议和担忧。
?操作符能否最终被Go语言采纳,以及它将如何影响Go语言的错误处理实践,还有待进一步的观察和讨论。无论如何,这个提案都为Go语言的未来发展提供了一个有益的思考方向。
你觉得这个提案如何,欢迎在评论区留下你宝贵的观点!
参考资料
[1]
Go团队核心开发者Ian Taylor: https://github.com/ianlancetaylor
[2]新的提案“proposal: spec: reduce error handling boilerplate using ?”: https://github.com/golang/go/issues/71203
如果本文对你有所帮助,请帮忙点赞、推荐和转发!
点击下面标题,阅读更多干货!
- 惊!Go在十亿次循环和百万任务中表现不如Java,究竟为何?
- Go map使用Swiss Table重新实现,性能最高提升近50%
Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。2025年将在星球首发“Go陷阱与缺陷”、“Go原理课”的专栏!此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!
著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。
Gopher Daily(Gopher每日新闻) - https://gopherdaily.tonybai.com
我的联系方式:
微博(暂不可用):https://weibo.com/bigwhite20xx
微博2:https://weibo.com/u/6484441286
博客:tonybai.com
github: https://github.com/bigwhite
Gopher Daily归档 - https://github.com/bigwhite/gopherdaily
Gopher Daily Feed订阅 - https://gopherdaily.tonybai.com/feed

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。