标签 RobPike 下的文章

关于Go错误处理新提案的一个想法:?操作符这样用行不行

本文永久链接 – https://tonybai.com/2025/02/08/personal-idea-about-using-question-mark-operator-in-go-error-handling-new-proposal

0. 背景

Ian Taylor在关闭了旨在消除Go错误处理样板代码的issue之后,又另起了一个“同名”的discussion。错误处理真不愧是Go社区呼声最高的问题,几天之内又收到了近500条回复!不过到目前为止,依然没有形成统一和高赞的意见。

关于error handling的样板代码过多,其实我个人是可以接受的,即便不做出任何改变也是ok的,估计Go社区与我有相同看法的也不在少数。比如就有人引用了Rob Pike的权威观点,并认为Go应该按照Rob大神的思路,保持Go语法稳定:
 

 
不过自然也会有另外一批人强烈希望错误处理的样板代码得到改进。

Ian Taylor在discussion中明确了该提案的目标是引入一种新语法,在不影响控制流清晰度的前提下,减少正常情况下检查错误所需的代码量
 
Ian最初的Proposal由于隐式声明变量err以及可选代码块等问题而备受“批评”,并且似乎该proposal违反了他自己提出的目标。
 
今天在discussion中看到一位名为Mukunda Johnson的gopher的评论,我觉得很有道理。其核心观点就是:尽量保持Go的传统语法形式。他还给出了期望中的语法示例:

// 当前错误处理样板代码过多的示例
f, err := open(file)
if err != nil {
   return err
}
defer f.Close()

if err = binwrite(f, signature); err != nil {
   return err
}
if err = binwrite(f, header); err != nil {
   return err
}
if err = binwrite(f, zeroSegment); err != nil {
   return err
}

for _, s := range segments {
   if err = binwrite(f, s); err != nil {
      return err
   }
}

if err = binwrite(f, footer); err != nil {
   return err
}

vs. 

// 使用新语法改进后的代码
f, err := open(file)?
defer f.Close()

binwrite(f, signature)?
binwrite(f, header)?
binwrite(f, zeroSegment)?

for _, s := range segments {
   binwrite(f, s)?
}

binwrite(f, footer)?

这给了我很大启发:我们可以引入?语法,但是如果结合原先err变量的声明形式岂不是更好!比如:

f, err := open(file)?

岂不是要比下面两种形式更好!

f := open(file)?

或

f := open(file)? err { }

通过仅引入一个问号(?)操作符,并避免引入过多的新语法形式,却能解决60%的错误处理样板问题。根据jba对Go开源代码中错误处理的抽样统计,超过60%的错误处理都是直接返回err,而没有对err进行任何修饰。此外,显式声明err可以最大程度地避免隐式声明带来的问题,同时提升代码的可读性。

因此,基于尽量使用已有Go代码风格、最大程度避免隐式声明,并仅解决最常见的错误处理样板代码的原则,下面我基于Ian提案的错误处理改进语法,谈点自己关于新?操作符使用的想法,大家看看是否可行。

1. 对于最常见的未经修饰的错误处理代码

err := SomeFunction2()
if err != nil {
    return err
}

或是

if err := SomeFunction2(); err != nil {
    return err
}

我们使用下面的新语法做等价替代:

err := SomeFunction2() ?

如果声明的错误变量名为err,也可省略赋值操作符左侧代码,从而简化为:

SomeFunction2() ? // 这里略带隐式

2. 如果函数返回值有多个,甚至有多个错误变量的情况

比如下面代码:

a, b, err0, err1, err2 := SomeFunction3()
if err2 != nil {
    return err2
}

我们可以将其改写为:

a, b, err0, err1, err2 := SomeFunction3()?

其语义是如果err2不为nil,返回err2,但前提要保证赋值语句的左侧的最后一个变量err2必须是实现error接口的类型的变量。

如果是像下面这样在err2 != nil时有多个返回值,又该如何处理呢?

a, b, err0, err1, err2 := SomeFunction3()
if err2 != nil {
    return a, b, err2
}

对于这种情况,我认为可以不在新方案的考虑范围之内,现在怎么写,请继续这么写。如果非要解决,请继续看后面支持可选代码块的情况。

实现以上两种情况,就能解决60%以上的错误样板代码问题了!

3. 对于对返回的error值进行修饰的情况

对于像下面两种对返回的error变量进行修饰的情况:

r, err := SomeFunction()
if err != nil {
    return fmt.Errorf("something failed: %v", err)
}

if err := SomeFunction2(); err != nil {
    return fmt.Errorf("something else failed: %v", err)
}

我的第一想法是保持现状 ,不在新方案考虑范围之内。

不过如果非要在新方案中解决,那就需要引入可选代码块(optional block)了!比如:

r, err := SomeFunction() ? {
    return fmt.Errorf("something failed: %v", err)
}

err := SomeFunction2() ? {
    return fmt.Errorf("something else failed: %v", err)
}

和Ian的原proposal中语法不同,这里我们依然显式声明了err,当然你也可以不用err这个名字,由于是显式声明,你用任何名字均可,比如:

r, e := SomeFunction() ? {
    return fmt.Errorf("something failed: %v", e)
}

myErr := SomeFunction2() ? {
    return fmt.Errorf("something else failed: %v", myErr)
}

这将避免隐式声明带来的诸多问题!

基于可选代码块,我们也可以处理一下前面提到的返回多个值的情况。下面代码

a, b, err0, err1, err2 := SomeFunction3()
if err2 != nil {
    return a, b, err2
}

可以改写为:

a, b, err0, err1, err2 := SomeFunction3() ? {
    return a, b, err2
}

这里加入可选代码块后,我建议开发人员负责显式调用return,而不是由?操作符来自动return,也就是说完全将控制权交给你。如果你没有在可选代码块中调用return,那么代码在执行完可选代码块中的代码后,还会继续向下执行。可选代码块相当于一个error handler,而不带可选代码块的情况,默认的error handler其实就是一个return err,伪代码类似这样:

err := SomeFunction2() ?

<=>

err := SomeFunction2() ? {
    return err
}

这样解释后,你是不是觉得在语义层面,不带可选代码块与带有可选代码块的情况就统一和一致了呢!

本质上来说,?+可选代码块仅是让你少敲了个if以及err != nil

4. 综合示例

Mukunda Johnson给出的示例其实已经可以很好地展示?操作符+显式声明err方案带来的消除样板代码的效果,这里再回顾一下(这里没用到可选代码块,因此代码显得格外清晰):

f, err := open(file)?
defer f.Close()

binwrite(f, signature)?
binwrite(f, header)?
binwrite(f, zeroSegment)?

for _, s := range segments {
   binwrite(f, s)?
}

binwrite(f, footer)?

此外,在原discussion中,另外一个gopher提出的示例,我们也可以用上面的想法改写一下:

// 最常见的情况
SomeFunc() ?

// 多个返回值,最后一个为error变量
a, err1 := SomeFunction2() ?

// 返回前对err进行修饰
err := SomeFunc() ? {
  return fmt.Errorf("oh no: %w", err)
}

// 显式声明避免变量遮蔽
err := SomeFunc() ?  {
  otherErr := OtherFunc() ?  {
    err = errors.Wrap(err, otherErr) // 在可选代码块中没有显式调用return,代码还会继续向后执行
  }
  return fmt.Errorf("oh no: %w", err)
}

5. 小结

再来简单总结一下上面想法中的语法形式的优势:

  • 与传统Go语法形式几乎一致,尽量避免引入过多新语法形式,在不使用可选代码块的时候,只是多了一个问号(?)。
  • 显式声明err变量,最大程度避免隐式声明带来的问题。
  • 专注解决最常见的错误处理样板情景,其他场景保持当前写法即可。
  • 即便引入可选代码块,本质上与不用可选代码块的语法在语义层面也是统一和一致的。

这一语法方案保留了原Ian提案中的优势,并能消除一些缺点,如变量遮蔽和隐式声明等。不过,仍然有些原proposal中的劣势问题无法完全消除,但这些问题显然不是主要关注点。

需要注意的是,以上想法目前仅停留在形式讨论层面,技术层面是否可行尚不确定。

大家认为我的想法可行吗?希望大家能提出更具建设性的意见^_^。


Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且,2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏!此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格6$/月。有使用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

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

Go,15岁了[译]

本文永久链接 – https://tonybai.com/2024/11/12/go-turns-15

虽然迟到了,但绝不缺席!新任Go技术负责人Austin Clements在Go语言15岁生日后的第二天,在Go官方博客上发表了庆祝文章“Go Turns 15”。在这篇文章中,Austin回顾了过去一年Go项目和社区的变化,以及Go团队的努力工作,并对Go的未来发展进行了展望。我在此对这篇庆生文进行了翻译,供大家参考。


Go,生日快乐!

周日,我们庆祝了Go开源15周年

自从Go诞生10周年以来,无论是Go语言本身还是整个世界都经历了巨大的变化。尽管如此,有些方面依然保持不变:Go始终致力于稳定性、安全性,以及支持软件工程和大规模生产

Go语言发展势头强劲!在过去五年中,Go的用户群增加了三倍多(译注:不知道这个数据从何而来),成为增长最快的编程语言之一。自十五年前诞生以来,Go已成为十大编程语言之一,并成为现代云计算的主要语言


来自TIOBE 2024年11月排行榜(译者配图)


来自Github Octoverse 2024(译者配图)

随着Go 1.22版本在二月份发布和Go 1.23版本在八月份发布,这一年可被称为“for循环之年”。Go 1.22将for循环中引入变量的作用域改为每次迭代,而非整个循环,从而解决了一个长期存在的语言“陷阱”。十多年前,在Go 1发布之前,Go团队对几个语言细节做出了决策,其中就包括for循环是否应该在每次迭代中创建一个新的循环变量。有趣的是,这次讨论非常简短且没有明确的意见。Rob Pike以他一贯的风格结束了讨论,只说了一个字:“stet”(保持原样)。结果也确实如此。尽管当时看似微不足道,但多年的生产经验突显了这一决策的影响。然而,在此期间,我们还构建了强大的工具来理解对Go的变更影响,特别是在整个Google代码库中进行生态系统范围的分析和测试,并建立了与社区合作和获取反馈的流程。在经过广泛的测试、分析和社区讨论后,我们推出了这一变更,并配备了哈希二分工具,以帮助开发者在大规模代码中精确定位受影响的部分。

对for循环的变更仅是是五年演进调整的一部分。这一变更的实现得益于Go 1.21中引入的向前兼容性,而这又建立在四年半前Go 1.14发布的Go模块基础之上。

译注:Go module首次在Go 1.11版本由Russ Cox设计和实现,Go 1.14版本首次宣布Go module具备生产使用的成熟度了。

Go 1.23在此变更的基础上进一步引入了迭代器和用户定义的for-range循环。结合仅仅两年半前在Go 1.18中引入的泛型!——这为自定义集合和许多其他编程模式奠定了强大而人性化的基础。

这些版本还带来了许多生产就绪方面的改进,包括备受期待的标准库HTTP路由器增强执行跟踪的全面重构,以及为所有Go应用程序提供更强的随机性。此外,我们的第一个v2标准库包的引入为未来的标准库演进和现代化建立了模板。

在过去的一年中,我们还谨慎地推出了Go工具的自愿使用的遥测系统。该系统将为Go开发者提供数据,以便他们做出更好的决策,同时保持完全开放和匿名。Go遥测最初出现在gopls(Go语言服务器)中,已经带来了许多改进。这项努力为使Go编程体验变得更加出色奠定了基础。

展望未来,我们正在不断演进Go,以更好地利用当前和未来硬件的能力。在过去的15年中,硬件发生了巨大的变化。为了确保Go能够在接下来的15年中继续支持高性能、大规模的生产工作负载,我们需要适应大型多核处理器、先进的指令集,以及在non-uniform内存层次结构中日益重要的局部性。其中一些改进将是透明的。Go 1.24将推出全新底层实现的map,以提高在现代CPU上的执行效率。同时,我们正在进行新的垃圾回收算法的原型设计,以适应现代硬件的能力和限制。一些改进将以新的API和工具的形式出现,以便Go开发者更好地利用现代硬件。我们正在研究如何支持最新的向量和矩阵硬件指令,以及应用程序如何构建CPU和内存的局部性。指导我们努力的一个核心原则是可组合优化(composable optimization):优化对代码库的影响应该尽可能局部化,以确保对其余代码库开发的便捷性不受影响。

我们将继续确保Go的标准库在默认情况下是安全的,并在设计上也考虑到安全性。这包括不断努力将内置的、原生支持的FIPS认证加密功能纳入其中,使得需要FIPS加密的应用程序只需简单切换一个命令行标志即可使用。此外,我们还在不断改进Go的标准库包,并借鉴math/rand/v2的例子,考虑在哪里可以引入新的API,以显著提高编写安全和可靠的Go代码的便利性。

我们正在努力使Go在人工智能领域表现更好,同时也让人工智能更好地服务于Go,增强其在AI基础设施、应用程序和开发者辅助工具方面的能力。Go是一种非常适合构建生产系统的语言,我们希望它也能成为构建生产级AI系统的优秀语言。作为云基础设施的可靠语言,Go自然成为大型语言模型(LLM)基础设施的理想选择。针对AI应用,我们将继续在流行的AI SDK中为Go提供一流的支持,包括LangChainGoGenkit。从一开始,Go就旨在改善端到端的软件工程过程,因此我们自然希望引入AI的最新工具和技术,以减少开发者的重复劳动,从而留出更多时间来进行更有趣的编程活动!

感谢您!

所有这一切的实现都离不开Go的杰出贡献者和蓬勃发展的社区。十五年前,我们只能憧憬Go所取得的成功以及围绕Go发展起来的社区。感谢每一位参与其中的人,无论贡献大小。我们祝愿大家在新的一年里一切顺利!


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商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

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

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 AI原生开发工作流实战 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats