十年来Go语言的亮点、挑战和普通之处

发表时间: 2023-07-21 11:16

【编者按】本文作者对他在十年前撰写的一篇名为 “Go 语言:优点、缺点和平淡无奇之处” 的文章进行回顾和更新,讨论了他的准确预测、Go 语言的变化以及他之前的疏漏。本文见证了 GO 语言这十年的演进历程。

原文链接:https://blog.carlmjohnson.net/post/2023/ten-years-of-go-good-bad-meh/

未经允许,禁止转载!


作者 | Carl M. Johnson 译者 | 明明如月
责编 | 夏萌
出品 | CSDN(ID:CSDNnews)

十年前,我撰写了一篇名为Go 语言:优点、缺点和平淡无奇之处(链接见文末)的文章。该篇文章在 2013 年冲上了Hacker News 的首页,并在/r/programming 上 收获了超过 400 条评论。尽管我未留下当时的分析数据,但我推测这应该是我最受关注的文章之一,而且这绝对是我第一次在写作中收获大量反馈的难忘经历。

如今,十年过去了。在这段时间里,我从一个出于娱乐心态只是试验 Go 语言的业余爱好者,转变为一名将 Go 列为主要编程语言的专业程序员。因此,我觉得现在回顾并分析我当年的观点,探讨我准确预测了什么、什么事情发生了改变,以及我忽视和犯下了哪些错误,这会是一次有趣的经历。你可以阅读或重新阅读原始博文,或者只需在这里阅读我对过去的回顾,无需深入解析那篇文章。你只需要知道,正如它的标题所示,我当时把我对 Go 语言的评价分为“优点”、“缺点”和“平淡无奇之处”。

对于任何希望将本文提交至社交媒体的读者,请注意,标题中的引号非常重要。切勿删除它们。

我的准确预测

我仍然认同我在"优点"部分列出的大部分内容。以下所有观点我都依然认为是对的:

  • Go 语言是为了在拥有现代版控系统的团队中开发大型项目而设计的

  • 通过大写字母来区分函数、方法、变量和字段的公有/私有状态

  • 将目录作为包管理的基本单元

  • 使用单一的二进制文件进行部署

  • go 工具运行速度快,内置了 go fmt、go doc 和 go test

  • 使用后置类型样式(var x int)而非前置类型样式(int x)

  • 有明确的变量声明(相对于 Python 的 = 进行隐式声明)

  • Go Playground

  • 逻辑类型名称(如 int64,float32 等,而非 long 和 double)

  • 提供三种基本数据类型:字符串、变长数组和哈希映射

  • 接口用于编译时的鸭子类型 - 不支持继承

至于其他部分,我们将在后续进行讨论。

发生的变化

过去十年中,Go 语言的最显著变化无疑是引入了泛型。

在我之前的文章中,我把缺乏泛型列为了 Go 语言的一大弊端:

在 Go 代码中,我们常会使用一些特殊的技巧,通过接口来避免使用泛型。但在某些情况下,你可能别无选择,只能将函数复制粘贴多次,以针对不同类型进行操作。如果你需要构建自己的数据结构,你可能会选择 interface {} 作为通用类型,但这会丧失编译时的类型安全性。反之,如果你想实现一个通用的 sum 函数,就没有理想的解决方案。

在 2022 年 2 月发布的 Go 1.18 版本中,引入了泛型特性。这基本上解决了我以前的遇到问题。例如,一个泛型的 sum 函数会像下面这样:

type Numeric interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float64 | ~float32}
func sum[N Numeric](vals ...N) N { var total N = 0 for _, val := range vals { total += val } return total}

到目前为止,泛型的引入无疑是一项重大的改进。

与 2013 年的 Go 语言相比,另一项重要变化是引入了 Go modules。我在过去的文章中提到,当时的 GOPATH 系统是 Go 语言的一大优点:

如果你运行 go get github.com/userA/project/ ,它将使用 git 从 github.com 下载项目,并自动将其放置到正确的位置。如果该项目中包含行import "bitbucket.com/userB/library" ,go 工具也能够下载并安装这个库到指定位置。因此,Go 语言一举具备了自身的优雅打包解决方案和一个现代化的、去中心化的 CPAN 或 PyPI :你已经在使用的版本控制系统!

Go modules 在原有的系统基础上,增加了指定导入包以及 Go 本身的版本需求的功能。虽然在从 GOPATH 切换到 Go modules 的过程中出现了一些波折和不满,但总的来说,过渡非常顺利,就我个人而言,我并未遇到太多实际问题。它的工作方式很有效,大多数时候你无需过多考虑它。

在我之前的文章中,我提到对于 Go 编译器只产生错误而不产生警告的情况我并未有太大的感触。但过去十年中,这个问题已经演变为 go vet 可以产生低误报率的错误,而其他警告则可以通过各种 lint 工具产生。所以,从技术角度来说,编译器依然没有警告,但在实际使用中,Go 的生态系统确实提供了很多产生警告的途径。这只是在你想要批评 Go 时,才会成为一个有趣的差异。

我的疏漏之处

在回顾我先前的文章时,我注意到并未涉及联合类型、交叉类型或者可选值这些概念。这反映出在过去的十年中,整个行业的状况变化剧烈。在当时,和类型及可选值仍被视为学术性的特性,除了 Haskell 这样的 ML 衍生语言,主流的 C 影响语言并没有涵盖这些特性。然而,自从 2014 年 Swift 和 2015 年 Rust 的诞生以来, 所有的语言都开始根据其是否解决了"空指针错误"(被比喻为十亿美元的错误)进行评价。

可惜的是,Go语言已经有太多使用 nil 值的代码,所以很难去掉 nil 值,以至于我们无法真正转向可选类型,至少在我们所了解的 Go 语言中是如此。然而,有一个通过限制接口值来添加求和类型 的提法,我认为这有很大的可能以某种形式实现。

值得一提的是,对语言的评价已经发生了变化。我曾经赞赏 Go 的类型推断为技术进步,而现在 Hacker News 的用户则认为 Go 的类型推断与其他语言相比还比较弱。选择一个不为人知的语言,或者坚持足够久,总会有一天你会成为别人诟病的目标。

再回首过去,我发现我原文中还有一个疏漏是没有提到 Go 1 保证。Go 1.0 发布时,Go 团队对源代码的兼容性做出了承诺。尽管一直都有警告、错误和例外存在,但在大多数情况下,他们极大地遵守了这个承诺。公正地说,当时,Go 1.0.3 是最新的版本,因此我无法预见 Go 1 的承诺将会持续十年以上,但我相信这对 Go 成为今天的语言发挥了关键作用。在 Go 1.0 之前,Go 团队经常对语言或标准库做出改变,为了跟上新标准,需要在代码库上运行 go fix。然而,自那时以来,只要你写的是 Go 程序,使用的是标准库,并且没有依赖于不安全的特性或者安全漏洞,你就能保证你的代码一直正常运行。

相比其他生态系统,这是一股真正的清新之风,作为开发人员,这是我最喜欢的事情之一。在其他编程语言中,升级到一个新版本(即使是一个次要版本),往往让人感到担忧,担心会有一些意想不到的问题出现。即使在升级过程中提前通过弃用警告声明了问题,仍然需要花时间来修复相关问题。但是使用Go,我不会担心这些。GO 语言团队非常为开发者着想。

我的观点更新

总体而言,我认为我之前的博文质量不错,没有什么明显的错误。不过,虽然并不完全否定我之前的有些看法,但我现在有了更深入的理解。

具体而言,在我描述的“优点”部分,没有什么真正的不足之处,但回顾过去,我对并发带来的权衡有了更深的认识。

我曾写到:

Go 的并发处理方式,就如同你在刚开始学习并发时所设想的那样,非常易于理解和使用。如果你想并发地运行一个函数,只需要使用 go function(arguments)。如果你需要让函数间进行通信,你可以使用通道,这些通道默认会同步执行,即在两端都准备就绪前,会暂停执行。

这个观点仍然是正确的,我依然认为在 Go 中处理并发比在 Python 中要简单得多。(顺便说一句,我写下这些观点是在 JavaScript 添加了 Promiseawait 之前。)然而,类型系统并不能阻止你创建数据竞争,因此在测试过程中你必须在依赖数据竞争检测器,如果你在不遵循公认的模式的情况下直接使用通道创建结构化并发,这会导致代码混乱。这是一种权衡,Go 给你充足的自由以至于可能自我陷入困境,但在大多数时候,当你只是编写网络服务器或类似的应用时,你可以获得并发的大部分好处,而不会遇到太多的麻烦。

就我列出的“缺点”部分来说,大部分实际上并未给我带来太大问题。我不确定为何我会过度担忧字符串类型无法定义方法。脚本无法以 #! 开头在实践中其实并不重要。Go 的语言设计并非完全遵循 DRYDon't repeat yourself,不要重复你自己)原则,但这更多的是一种权衡,而非“缺点”。

虽然缺乏泛型有时会导致问题,但通常有明确的解决办法,例如使用动态类型,使用反射,或者编写一个代码生成器。我很高兴 Go 现在引入了泛型,但事实上,没有泛型时我们只会偶尔遇到真正的问题。我非常期待看到泛型如何影响 Go 的未来发展,尤其是在 Go 团队正在研究迭代器的情况下,但我确实有些担心可能会出现“泛型的滥用”,即在普通接口已经能够很好工作的地方过度使用泛型。我们将拭目以待。

我在“无所谓”的部分列出的大部分事项,实际上更多的是权衡,然而事后看来,我认为 Go 在它所选择的设计范围内做出了更好的决定。没有异常机制是一种权衡,虽然我可能希望 Go 有像 Zig 的 tryerrdefer 那样的功能,但实际上,当前的方式是可行的。尽管使用 if err != nil 是目前为止最差的选择,除去所有其他不定期试验的系统,但事实证明,它能促使用户代码有用的演变。

如 Matklad 在一条最近的评论中所说,

看来在错误处理上,我们(开发者社区)大致达成了共识。Midori、Go、Rust、Swift、Zig 在设计上有着相似之处,这并非完全是检查异常,但却非常接近。

1、有一种标记函数可能会失败的方式。这通常是返回类型的属性,而不是函数本身的属性(Rust 中的 Result,Go 中的错误对,Zig 中的 ! 类型,以及 Midori 和 Swift 中的 bare throws decl)

2、我们不仅标记抛出异常的函数声明,还会标记调用处(Midori、Swift、Zig 中的 try,Rust 中的 ?,Go 中的 if err != nil)。

3、默认存在一个 AnyError 类型(Go 中的 error,Swift 中的 Error,Rust 中的 anyhow,Zig 中的 anyerror)。一般来说,区分是否有错误的价值,大于详尽地指定错误集合。

这种观点对我来说是合理的。Go 的错误处理相比其他语言更为繁琐,但从结构上来看,它们在底层有许多共同之处。

我曾把 Go 缺乏操作符重载、函数/方法重载或关键字参数等特性归入“无所谓”的部分,但现在我对这种状态感到非常满意。我希望 Go 有操作符重载的唯一情况是 big.Int ,我希望有一天这些特性会被添加到语言本身中。关键字参数可能是好的,但实际上,结构体就够用了,如果有真正棘手的情况,总是有 方法链的构建器可用。

总结

在撰写这篇文章前, 我原以为读者们会关注我对面向对象编程的批评,然而,实际上他们更在意的是我用“白痴”一词描述争论公有/私有字段问题的人。

我在文章中提出的最大疑问可能是 Go 语言对接口的运用以及继承特性的缺失。我认为这是一个好主意,但也欢迎实践的检验,并期待听到不同的观点。Hynek Schlawack 在 2021 年发表的一篇极佳的文章解释了为什么接口的运用以及没有继承可以被看作是 Go 的优秀设计选择。简单来说,只有在子类覆写超类方法的情况下,继承才有意义。在这种情况下,Go 的类型嵌入表现得十分出色。因此,我终于激起了我期待已久的讨论,尽管这花了大约八年的时间。

直到 2019 年,Dan Abramov 才对 Hacker News 的写作经验进行了深入的解读:

恭喜你!

你的项目成为了热门新闻聚合器的头条。社区里的一些知名人士也在推特上提到了它。他们在说些什么?

你感到心情沉重。

并不是说人们不喜欢这个项目。你清楚项目存在权衡,也期待人们会对此进行讨论。但是事情并未如预期发生。

然而,评论在很大程度上并不关心你的想法。

最热门的评论主题是围绕 README 示例中的编码风格。它演变成了一个关于缩进的争论,有一百多条回复,并对不同的编程语言如何处理格式进行了简短的历史回顾。其中必然会提到 gofmt 和 Python。你使用过 Prettier 吗?

你感到困惑,关闭了标签页。

发生了什么?

也许是你的想法简单地不像你想象的那样有趣。也可能是你对偶然访问的人解释得不够好。

但是,你可能没有得到相关反馈的另一个原因。

我们更偏向于探讨那些相对容易讨论的话题。

我已经接受了这样一个事实:论坛上的人们(包括我自己)更容易关注自己已经想到的事情,而非文章所讨论的主题。这就是现实,短期内恐怕无法改变。

我认为 Rachel 的 Run XOR Use 规则 (要么使用一个聊天窗口或留言板,但不能同时做两者。这是为了避免冲突和分散注意力。)同样适用于撰写和讨论博客文章。如果你发布了文章,你必须预备好讨论会扩展到你自己都未曾预见的领域。

对于 Go 语言,我现在的满意度超过了以往的任何时期。虽然它的速度并不如 Rust,但已经超过了我的需求,同时并无过度的模板代码或抽象概念诱惑。我认为 GO 语言团队具有良好的洞察力,他们总是稳健地向正确的方向前进。我期待看到它未来的发展。

预测总是很难的,尤其是对于未来的事物。十年后我还会使用 Go 吗?我无法确定。预期中的十年总似乎比回顾中的时间要长。也许那时我们只是在检查由 AI 生成的代码的输出,虽然这听起来有些消极。但至少现在,我乐意将它作为我的主要编程语言。

以下是那篇文章的原始结论,以及我在 2023 年的更新:

Go 真的很棒,它已经成为我的日常语言(它曾经是 Python ),而且它绝对是一种趣味十足的语言,非常适合处理大型项目。如果你有兴趣学习 Go,我建议学习下这个简单的教程,然后在将测试程序输入到 Playground 中运行,并且认真学习 GO 语言规范。该规范简洁易读,是学习 Go 非常好的学习材料。

让我们在 2033 年再会,届时我们将会在 “““Go 语言:优点、缺点和平淡无奇之处” 十年"十年” 中相见。

你也正在使用 GO 语言编程吗?你认同作者的说法吗?你对 GO 语言怎么看?欢迎在评论区讨论。

参考链接:

  1. Go 语言:优点、缺点和平淡无奇之处:https://blog.carlmjohnson.net/post/google-go-the-good-the-bad-and-the-meh/

  2. Hacker News:https://news.ycombinator.com/item?id=5200916

  3. John Carmack 是否阅读过这篇文章:https://twitter.com/ID_AA_Carmack/status/1293311943995002881

  4. 原始博文:https://blog.carlmjohnson.net/post/google-go-the-good-the-bad-and-the-meh/

  5. Go Playground:https://go.dev/play/

  6. Go 1.18 版本中:https://blog.carlmjohnson.net/post/2021/golang-118-minor-features/

  7. 下面这样:https://go.dev/play/p/R-QDDqcp3w9

  8. 十亿美元的错误:https://en.wikipedia.org/wiki/_pointer#History

  9. 通过限制接口值来添加求和类型:https://github.com/golang/go/issues/57644

  10. 选择一个不为人知的语言,或者坚持足够久,总会有一天你会成为别人诟病的目标:https://www.stroustrup.com/quotes.html

  11. Go 1 保证:https://go.dev/doc/go1compat

  12. Go 1.0.3 是最新的版本:https://go.dev/doc/devel/release#go1

  13. 公认的模式:https://blog.carlmjohnson.net/post/share-memory-by-communicating/

  14. 结构化并发:https://github.com/carlmjohnson/flowmatic

  15. 导致代码混乱:https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/

  16. 实践中其实并不重要:https://github.com/carlmjohnson/go-run

  17. DRY:https://en.wikipedia.org/wiki/Don%27t_repeat_yourself

  18. 用户代码有用的演变:https://blog.carlmjohnson.net/post/2020/working-with-errors-as/

  19. Matklad:https://matklad.github.io/

  20. 一条最近的评论:https://lobste.rs/s/aocv9o/trouble_with_checked_exceptions_2003#c_bsxqyu

  21. big.Int:https://pkg.go.dev/math/big#Int

  22. 添加到语言本身中:https://github.com/golang/go/issues/19624

  23. 方法链的构建器:https://blog.carlmjohnson.net/post/2021/requests-golang-http-client/

  24. 这篇文章:https://blog.carlmjohnson.net/post/google-go-the-good-the-bad-and-the-meh/

  25. 一篇极佳的文章:https://hynek.me/articles/python-subclassing-redux/

  26. 深入的解读:https://overreacted.io/name-it-and-they-will-come/

  27. Run XOR Use 规则:https://rachelbythebay.com/w/2021/05/26/irc/

  28. 虽然这听起来有些消极:https://blog.carlmjohnson.net/post/2016-04-09-alphago-and-our-dystopian-ai-future/

  29. 那篇文章:https://blog.carlmjohnson.net/post/google-go-the-good-the-bad-and-the-meh/

  30. 学习下这个简单的教程:http://tour.golang.org/

  31. Playground:http://play.golang.org/

  32. GO 语言规范:http://golang.org/ref/spec