本期深入研究 Go 堆栈的工作原理以及作为程序员为什么要关心它。
本篇内容是根据2023年3月份#288 A deep dive into Go's stack音频录制内容的整理与翻译
过程中为符合中文惯用表达有适当删改, 版权归原作者所有.
Mat Ryer: 大家好,欢迎收听 Go Time。我是 Mat Ryer。今天我们要讨论的是 Go 的栈。究竟什么是栈?它的作用是什么?我们作为程序员需要多关注它?要有效编写程序,我们需要了解多少?今天我们将深入探讨这些问题。今天和我一起主持的是 Kris Brandow。你好,Kris。
Kris Brandow: 你好,Mat,你好吗?
Mat Ryer: 还不错。最近你的大楼里有什么戏剧性的事情发生吗?
Kris Brandow: 嗯,不幸的是有。最近纽约市发生了一些不幸的事故,而我也受到了影响。不过现在一切都好了,我们都没事了。
Mat Ryer: 好的,太好了。但你没有导致这些事故吧?
Kris Brandow: 没有。
Mat Ryer: 嗯,那就好。今天我们还请到了 Yarden Laifenfeld。你好,Yarden。Yarden 是 Rookout 的软件工程师,或者说从周一开始是 Dynatrace 的软件工程师,你在那儿开发了一个用于 Go 的生产级调试器。这真是非常激动人心的项目,我们肯定会讨论到这个。另外,你也是 GopherCon Israel 的组织者之一,同时也是 Women Who Go Israel 小组的一员。欢迎你,Yarden。
Yarden Laifenfeld: 你好,谢谢你邀请我。
Mat Ryer: 非常荣幸。而且我们还请到了来自 Go 团队的 David Chase。David 已经在编译器和运行时方面工作了大约 40 年,并且自 2015 年起在 Google 开始为 Go 工作。我想那已经是 8 年前的事了,David。欢迎来到 Go Time。
David Chase: 你好。我不知道该说什么。
Mat Ryer: 不,你好就够了,完全没问题。不过你有个非常有趣的个人简历,你种植百合花,而且你还是北美百合花协会的评委。
David Chase: 那是很久以前的事了……我们以前去度假---
那时候我们有工作,有假期,但他们让我开车。我太太一个人计划了一些日子,而我只提前五秒钟计划。
Mat Ryer: 就是即时规划。
David Chase: 是的,完全是这样。于是我们开车时看到了一个标志,上面写着“下一路口有百合花和秋海棠”。我们想“哦,挺酷的,我们喜欢百合花。”于是我们就去了。结果我们遇到的那个人是一个非常有名且极具创新性的百合花育种专家。我们觉得“哦,真是太酷了。”他给了我他的目录,我订购了一些百合花开始种植……就这样,我一直种了很多年……我很喜欢它们,本地有一个协会,他们说“哦,我们需要一些帮手,需要更多的帮手,还需要评委……”我经过了一个多年的培训过程,包括考试和实地评审,最终成为了评委。我必须强调,我只是一个初级评委。你会惊叹于你学到的东西,但同时那些真正厉害的人让你觉得“天哪,你真的能做得这么好。”这有点奇怪。有时你和别人一起出去,他们会问你关于百合花的问题,你就会开始信口开河地解释这个那个。“哦天哪,我竟然说了这么多。”然后你会觉得“好吧,挺酷的。”
Mat Ryer: 真是太棒了。百合花难种吗?
David Chase: 并不特别难,这也是我喜欢它们的原因之一。我可以集中精力做些重要的工作,然后我就可以把它们放在那里,自然生长。在美国,有一种从欧洲引入的害虫,曾经让种植百合花变得非常困难。但后来引入了一种特定的黄蜂,专门捕食这种害虫的幼虫,现在这种黄蜂已经在这里定居了,这让种植变得很容易。你只要种下它们,它们就会长得很好。
Mat Ryer: 太神奇了。
David Chase: 我该停下来了,我可以再说很久。
Mat Ryer: 不不不,也许我们应该开一个“百合时间”节目。[笑声]
David Chase: 好,我们可以做这个。
Mat Ryer: 对,下次我们就做这个。哇,真是太棒了。Yarden---
其实你和 David 有一个共同点,你们都喜欢自行车,喜欢骑车。Yarden,你经常骑车吗?
Yarden Laifenfeld: 我写这个是回应 David 提到的自行车。
Mat Ryer: 嗯,我又读了一遍。你说你不擅长骑车。
Yarden Laifenfeld: [笑] 是的,这是真的。我不会说我不擅长……
Mat Ryer: 但你确实这么说了。
Yarden Laifenfeld: ……不过我确实不擅长。[笑] 我并不特别擅长骑车。我会骑,也确实骑车。
Mat Ryer: 你还能要求什么呢?
Yarden Laifenfeld: 完全正确。
Mat Ryer: 是的,不过修车可能也挺重要的。
David Chase: 修车也是。
Yarden Laifenfeld: 有人帮我修。
Mat Ryer: 对,可以让 David 帮你修。David 也会修自行车。如果你想种百合花或者需要修自行车,David 是你(要找)的人。
David Chase: 也许吧。
Mat Ryer: Yarden,在你开始使用 Go 之前,你写过 Java、Ruby、C#
、C++、Python……是什么让 Go 吸引了你的注意?
Yarden Laifenfeld: 实际上我现在还在用这些语言。在 Rookout---
或者我应该说 Dynatrace---
我们现在支持所有这些语言。但我的主要工作还是 Go。我在使用 Go 之前写过 C,我非常喜欢它---
它的简洁性以及接近底层硬件的特性。我知道它实际上并没有那么接近底层,但它已经是现代人们能接触到的最底层的了……
Mat Ryer: 对,相对来说是这样的。
Yarden Laifenfeld: 是的,完全正确。所以我更喜欢 Go 的那种简洁性,而不是那些更高级的语言。
Mat Ryer: 那么,在这些语言中,你最喜欢哪一个呢?
Yarden Laifenfeld: 肯定是 Go 或者 C。
Mat Ryer: 很不错的回答。好的,那我们开始吧……我想从基础开始。什么是栈?它到底是什么,它的作用是什么?
David Chase: 在 Go 中,栈有点像一个内部的 slice(切片)。它的工作方式非常类似。栈有一个容量,它是从高地址分配到低地址,而不是像 slice 那样从低到高。而每当你调用一个函数或方法时,栈就会向低地址扩展一个固定的量。每个函数有它自己的固定栈帧大小。栈用于临时存储局部变量以及需要溢出到内存中的临时变量---
这些变量会被存储在栈中。根据你的架构的调用约定,你可能会在栈空间中传递给你调用的函数和方法参数。从 Go 1.17 开始,在某些架构上我们会使用寄存器来传递参数……但我们仍然会保留栈空间来存储这些寄存器的溢出值。
栈与 slice 的不同之处在于---
其实,这不完全对。slice 也有容量,但你可以不断地追加数据。如果你追加的数据超出了容量,slice 会重新分配一个更大的 slice。而在栈的情况下,Go 不同于很多编程语言。它会分配一个新的栈,并将旧的栈复制到新的栈中。
Go 的一个特别之处在于,它会记录栈上所有可能存在的指针位置。当你复制栈时,这些指针都会被更新。因此,你的程序完全不知道这一过程的发生。你只是调用了一个函数,栈复制到了一个新的位置,变得更大了,所有指向栈的内部指针都在复制过程中更新了,然后程序继续运行。
Mat Ryer: 这是一个昂贵的操作吗?
David Chase: 它确实有点昂贵……但实际上它只是内存复制,然后扫描栈并解释指针。不过你不会经常这样做。它有一种滞后效应。栈会保持较大的状态,直到垃圾回收器检查一个 goroutine 的栈时发现“哇,我们分配了你 1 MB 的栈空间,但你只用了 10 KB。我觉得我们应该收回一些空间。”然后它会修复栈并把你放回到一个较小的栈中。所以增长栈的操作是昂贵的,但你不会经常遇到这种情况,除非栈增长到很大。
一种替代实现是分段栈,这种方法以前在其他编程语言中也有使用过。在这种方式下,你不会重新定位旧的栈,而是分配一个新的栈段。但是这种方式有一个滞后问题和交叉问题。如果你恰好在调用很多函数的地方遇到边界,你总是会碰到它,因为它不是一个平滑的增量,而是“哦,我撞到了尽头,需要做一些额外的操作。” 即使你已经预留了下一个栈段并打算重用它,你仍然会碰到这个边界。这种操作非常昂贵,所有我知道使用过这种分段方法的人,除非他们有很好的理由继续使用这种方式---
而 Go 不需要这些理由---
他们最终都会转向连续的栈并进行复制。
Kris Brandow: 当垃圾回收器缩小栈时,是在 goroutine 完成任务之后吗?还是说会暂停 goroutine?这个操作具体是在什么时候进行的?因为你提到我们在函数调用时扩展栈,这是一个干净的时刻。那么栈缩小时也会有类似的干净时刻吗?
David Chase: 当然了,如果你的 goroutine 暂停了,也就是它没有在实际运行。它可能处于可运行状态,但没有被分配到线程上实际运行。在这种情况下,我相信如果它处于一个干净的停顿点,栈就可能会被缩小。这里我们得谈一下抢占。如果你要抢占一个 goroutine---
抱歉,应该说如果你需要抢占一个 goroutine,比如你需要进行垃圾回收,那么垃圾回收器需要与线程进行握手。这里有两种握手方式:合作式和非合作式。合作式抢占会发生在几乎每个函数的入口序列中,它会检查栈是否溢出。而当运行时需要中断时,它会谎报栈边界,goroutine 会说:“哦,看来我需要一个新的栈。”然后它进入该代码,发现实际上它是因为其他原因被中断,某些人需要与它交互。这就是一个干净的合作式抢占,所有指针的状态都是已知的,这时你就可以选择缩小栈。因为如果需要增长栈,也是通过这种方式,所以这是合理的。如果你不能找到指针,你也无法增长栈。
非合作式抢占发生在你有一个长时间运行的紧密循环时。我们不会在循环的回边上进行检查。我们会评估是否应该在这里显式检查,但我们没能将其成本降低到足够低。所以我们做的是中断 goroutine,并在某些地方无法中断时记录这些地方,检查并判断“我是否处于一个足够安全的地方?” 这不是一个你知道一切的安全点,但它是一个足够安全的地方。你可能不知道所有的细节,但你知道在那个点上进行垃圾回收是可以的,检查栈也是可以的,但你并不知道最后一个栈帧中的所有指针信息,所以你不能移动栈。抱歉说了这么多,但这是有原因的,并且比八年前复杂了很多。
Mat Ryer: 听起来绝对不简单。你提到的一个有趣的点,Yarden,也许你可以对此做些解释……你提到栈会增长到较低的内存地址。这是怎么回事?它是怎么做到的?它真的只是预留了一堆内存,然后向后工作吗?这样做的好处是什么?
Yarden Laifenfeld: 我首先要说的是,Go 的栈运作方式与普通栈非常相似。我的意思是,即使你用 C 写一个二进制文件,其中没有运行时去控制栈和类似的东西,你依然会有一块内存区域叫做栈,它的工作方式非常类似,其中很多东西也是类似的。所以 Go 真正模拟了这种行为,这很有道理,因为这是一个好概念。
在一个普通的---
我说普通的意思是非 Go 或非托管的二进制文件中,栈确实是从高地址向低地址增长的……因为你有一大块内存,或者你有一大段内存,其中有不同的部分,栈是其中的一部分,堆是另一部分,它们会相互接近。所以堆从低地址向高地址增长,栈从高地址向低地址增长,它们会越来越接近。
所以我说 Go 也是这样做的……关于分配的问题是---
David,如果我错了请纠正我。通常当需要时它会分配一个栈,但如果我记得没错的话,它通常会把栈保留起来。已经分配的栈会有很多重用的情况。
David Chase: 我不认为它有意图地重用栈。但可能会发生这种情况。这是运行时中我不太了解的部分。垃圾回收器会倾向于重用较小的栈,因为它会认为“这是一个大小类别”。因此任何这个大小的东西都会进入一个 4k、8k 或 16k 的小块中,然后下次需要相同大小的东西时,它就会再次使用这些小块。
Yarden Laifenfeld: 哦,那很酷。我以为这是专门为栈设计的,但我猜这适用于所有小块。
David Chase: 可能是。这是我不太了解的一个部分。
Kris Brandow: 听起来栈就像是某种特殊内存空间中的切片,它们专门用于 goroutine。这听起来栈没有那么特别。
David Chase: 有点像。栈的末尾有一些额外的信息,比如“嗯,我们把 G 放在哪里了?” 每个 goroutine 都有一个叫做 G 的东西,或者叫 G 结构。我其实不知道我们是把它放在栈的底部还是末端。
Mat Ryer: G 代表什么?
David Chase: 我想是 goroutine。
Mat Ryer: 很酷的名字,是个好名字。
Kris Brandow: [笑]
Mat Ryer: 嗯,如果我看代码时看到一个叫 G 的结构体……显然,它是那个领域中的一个概念。
Kris Brandow: 也有一些其他的单字母东西,对吧?比如 G、P 和 M…
David Chase: 是的……
Yarden Laifenfeld: 问题是你怎么给你的变量类型命名为 G?
Mat Ryer: 小写 g?
Yarden Laifenfeld: 你会叫它 g 吗?
Mat Ryer: 也许读作 Gee?
David Chase: 不,就是 G。
Mat Ryer: 就是 g。
David Chase: 就是 g。
Mat Ryer: 是的。但这些是主要的概念吗?因此它们应该被赋予这样的地位?因为我觉得这有点像是权力的象征---
一个结构体仅用一个字母命名,听起来有点霸气。是不是有点像在炫耀?
David Chase: 好吧,这是在运行时中的,所以有它自己的命名空间。它不会污染其他人的命名空间。你可以有你自己的 g,你可以有你自己的 m 和 p。这是允许的。
Mat Ryer: 是的。但问题是,你应该这样做吗?这才是问题所在,不是吗,David?
Yarden Laifenfeld: 我认为这些是基础概念。我学习 Go 运行时的方式基本上是通过阅读代码,试图理解发生了什么。你们在维护可读代码方面做得相当不错,但有些地方仍然很难理解,尤其是当我不知道为什么要这样做时。所以我通常会通过阅读那些编写代码的人写的东西来补充我的理解。所以当你开始深入运行时的代码时,你会发现很多地方会谈到 p、m 和 g,因为这是 Go 如何运行、如何如此高效以及如何实现 goroutines 或轻量级线程的基础。这一切都是从这里开始的。
Mat Ryer: 是的。当然,Go 是开源的,这一点非常棒。我们可以去看这些代码,实际上深入研究这些代码。而且这些代码是用 Go 写的。听起来这不是最容易理解的代码库……但我们有机会理解它。不过 Yarden,你是否经常需要深入研究这些代码,鉴于你所从事的工作?
Yarden Laifenfeld: 我确实需要经常研究这些代码。
Mat Ryer: 哦,你很喜欢这样做。
Yarden Laifenfeld: 是的,这非常有趣,因为它真的非常复杂,而且其中的实现方式非常惊人。因为我也在写 Go 代码,所以通过阅读那些运行我代码或编译我代码的代码,我就能更好地理解我的代码发生了什么。理解的层次非常多,这让我成为了一个更好的开发者,也让我觉得很有趣。
Mat Ryer: 那么你会建议人们为了这个原因去深入学习这些代码吗?还是说你可以成为一个足够好的 Go 程序员,而不用了解这些东西,直接让 David 他们去操心这些事?
Yarden Laifenfeld: 我认为这因人而异。如果你刚开始接触这门语言,那么深入研究它的内部机制或如何运行并不是正确的方式。但我确实认为,如果你已经使用 Go 一段时间了,或者它是你工作中的一个重要部分,那么深入了解它可能会让你成为一个更好的开发者。因为它不仅可以帮助你理解一些事情,还能帮助你避免因为使用不当而产生的一些常见但仍然是错误的使用方式。
从积极的方面来说,它还可以让你的代码变得更好,因为你可以通过了解运行时的一些小细节以及运行时的工作原理来提高性能。如果你了解不同的内存区域,你可以控制哪些东西放在哪里,诸如此类的事情。所以,对于更高级的开发者来说,我认为这很重要。
Kris Brandow: 我认为了解一些历史知识---
在过去,也许这会更有用。比如我们谈到这些 g、m 和 p……m 代表线程,我不知道为什么不叫它 t,但无所谓……p 实际上是处理器上的核心。在 Go 1.5 之前,默认情况下你只有一个 p。所以即使你有很多 goroutines 在运行,你实际上还是一次只做一件事;你并没有进行并行执行。我记得在我刚学 Go 时,这一点让我很惊讶。我以为自己同时在做很多事情,但实际上它们只是并发运行,而不是并行运行。我想当你开始研究运行时的东西时,你会看到这些差异在哪里,因为这些概念真的很难理解。我记得第一次看 Rob Pike 的演讲《并发不是并行》时,我觉得“这真的很难理解。”但当你能亲眼看到这些东西时,我觉得这会有很大帮助。
Yarden Laifenfeld: 是的,我完全同意。而且我觉得这让它更贴近我们开发者的层面。当我想到 Go 的开发者时,他们在我心里是那种掌握了一切知识的虚幻生物,知道如何创建完美的编程语言---
Mat Ryer: 你看过我的代码吗?[笑声] 哦,你是说 Go 团队。
Yarden Laifenfeld: 是的,不是个人开发者。我说的是 Go 团队。
Mat Ryer: 嗯,明白了。
Yarden Laifenfeld: 不过说真的,我会想,“这些人知道一切,知道所有的工作原理……”然后我阅读代码时发现,“哦,等等,他们的代码和我写的差不多。” 我写代码,他们也写代码,我可以读懂他们写的代码。突然之间,从社区的角度来看,我觉得这很酷,因为他们也是社区的一部分,我们都是其中的一员。
Mat Ryer: 是的,我能理解这种感觉确实存在。不过,Yarden,别忘了你非常聪明。并不是我们所有人都如此。
Yarden Laifenfeld: 哦,抱歉,Mat。
Mat Ryer: 不,不是我……我是在说 Kris。
Kris Brandow: 是在说我?!
Mat Ryer: 我只是开玩笑。当然不是。
David Chase: 说的是大家。
Mat Ryer: 是的……其实,我记得 George Carlin 曾说过,“想象一下一个普通聪明的人,然后意识到有一半的人比这个还笨……”这虽然有点残酷,但也挺有趣的。 (译者注: George Carlin是美国喜剧演员,社会活动家,批评家)
David Chase: 我觉得这有点刻薄……
Mat Ryer: 是的,有点。
David Chase:另一种看法是你很忙,有很多事情要做,所以有些事情会被你忘记。
Mat Ryer:完全没错。这就是为什么我的袜子穿反了。我太忙了。
David Chase:是啊,你有比袜子方向更重要的事情要处理。
Mat Ryer:裤子。
David Chase:裤子。
Kris Brandow:我觉得我们大家---
这有点哲学意味,但我认为我们每个人都在不同的方面聪明。我觉得这就是你在说的,Yarden,当你看到 Go 团队时。从外面看,感觉是“哇,他们对一切都了如指掌。” 但其实不是的,有些人对编译器非常熟悉,有些人对运行时非常了解,他们对某些部分非常在行,但对其他部分一无所知……他们依赖团队的其他成员来填补信息。
Yarden Laifenfeld: 这其实让人有点安慰。
Mat Ryer:是的,绝对是这样。那么,David,当你加入 Go 团队时,你之前写 Go 写了多久?
David Chase:哦,差不多是零天。
Mat Ryer:那你一定是通过了非常厉害的面试。
David Chase:有个规定,当时有人告诉我,我们可以公开谈论这个……面试的规则是---
怎么礼貌地说呢?不要在面试中胡扯。
Mat Ryer:对,这是普遍的好建议。
David Chase:坚持你所知道的内容,这就是你被评估的内容。你不会因为你不知道的东西被评估。评估的是你在你擅长的事情上有多好。所以我当时不知道任何 Go,但这没关系……
Mat Ryer:你带了一朵百合花去吗?
David Chase:不是为了面试。我确实带过几次……你在花园里有一朵漂亮的花,就把它带进来,感觉很好。
Mat Ryer:那真的很不错。
David Chase:是的,所以我那时候是零天。我开始学习 Go---
这不是一门难学的语言。我遇到了一些小麻烦---
我在想,怎么向一个真正的初学者解释这个呢?有些类型有点像引用类型,比如切片和映射。映射实际上是引用类型。如果你传递一个映射并修改它,你会看到这些修改。如果你传递一个切片并修改它,你会看到部分修改;但如果它增长了,你就看不到了。你只能看到你传递的那部分数据。对我来说,这有点毛骨悚然……你看不到扩展部分,你只能看到对传递部分的更改。
Mat Ryer:嗯,这确实有点不寻常。
David Chase:是的,这是语言中一直让我觉得有点奇怪的部分……但当你看现实生活中的问题时,似乎从来没有人搞错过。我不太明白。也许人们只是---
怎么说呢……我总是有点担心可能出现的病态问题是什么?所以我立刻想到“哦,他们可能会这样做,这样就会出问题。” 但实际上人们并不会这么做,所以问题不大。
Yarden Laifenfeld:也许人们在第一次尝试时受过伤害,然后他们找到了一种可行的方式,并一直坚持使用它。
David Chase:我甚至不记得听说过有人因此受伤。这就是奇怪的地方。我有一个朋友,他以一种非常不同的方式在使用 Go……他有很深的编程语言背景,曾是助理教授,曾在 IBM 研究院工作……当你向他解释这些事情时,他会说:“这太糟糕了。但它从来没有成为问题……”[笑声] 所以这有点奇怪,没有人似乎搞错过。
所以是的,我很快就学会了这门语言……这很好,因为我正在为它编写一个编译器,所以我确实需要知道它是如何工作的。
Mat Ryer:是的,但正如 Yarden 所说,这突显了一个重要的教训,那就是:你需要擅长学习如何学习。这才是重要的技能。你不需要知道所有的事情,也不需要把所有事情都记在脑子里。你需要能够有针对性地学习,基于你正在处理的事情,或者你正在解决的问题……因为这也是很多初级开发人员的一个问题---
正如 Yarden 所说,他们看到有人在做演讲,演讲内容非常丰富。显然,他们要么做了大量的研究,要么有直接的经验。最好的演讲通常是有人在讲述他们真实经历过的事情。所以他们为了做这个演讲,专门研究了这个领域。这也是一个给演讲的好理由……如果你真的关心并想学习某些东西,准备一个演讲是一个很好的方式。但你不需要知道所有的事情,并把所有的东西都记在脑子里。我觉得这对每个人来说都是一个很好的提醒,特别是当你是新手时,因为你没有太多的经验来处理这些事情,这看起来别人就像天才一样。
Yarden Laifenfeld:是的,我有很多辅导非常非常初学者的经验。而我遇到的最大问题是,他们不敢尝试他们想到的解决方案……他们会坐在电脑前,看着屏幕。我会过去问:“怎么了?” 他们会说:“我不确定该怎么做。” 我会问:“那你是怎么想的?” 他们会告诉我解决方案,可能是对的,也可能是错的,但我会说:“好,那你为什么不写下来试试呢?” 他们没有真正的理由,只是说:“哦,我不知道我可以那样做。” 或者“我不知道那些东西在那里。” 我觉得这是成为更好开发者的第一步,就是通过尝试和学习来进步。
David Chase:绝对是“哦,我不知道,真的吗?我可以那样做?好的……”
David Chase:即使是 Go 内部代码也是这样。 [笑]
David Chase:是的,有些部分有点吓人……并发的东西就是这样---
或者说是那些奇怪的调优,比如“我需要与那边的线程同步……我觉得我会稍微自旋一下。” 有人发现,“是的,这是个好主意。” 但有时候如果出现了某种奇怪的架构变化,自旋就不合适了,比如你的处理器的缓存出问题了,或者你自旋的那个东西没有进入你的缓存,导致你有个大问题。
Kris Brandow:我觉得 Go 很擅长让你仍然可以接触到那些可怕的东西,但不会一开始就把它们抛给你。我觉得你说到切片,David,我觉得人们不会搞砸切片的原因之一是,他们很晚才学到数组。在你学语言的初期,数组并不是经常出现的东西。通常是“这是个切片,在其他语言中你会有这样的东西,叫做数组。把它当成那样用。” 我觉得如果你一开始就告诉他们“好吧,有这些切片,还有这些数组。它们很相似,但不一样。一个可以增长,另一个不能。” 这可能会让切片更加令人困惑。但只是告诉你“这是一个东西,是一组数据项。就这样用它。” 人们通常会觉得:“好吧,我就这样用。”
David Chase:你可以在末尾添加新的数据项,一直这样做,没问题。
Kris Brandow:是的,我同意这有趣的地方在于,人们能够直观地理解“哦,如果我修改已有的内容,我会在两边都看到修改;但如果我扩容了这个东西,那就是一个新的东西。” 但我觉得 API 在这方面也很有帮助。比如“哦,如果我 append,我会得到一个新的切片,这与我之前的切片是不同的。之前的切片保持不变。”
Mat Ryer:是,这是真的。不过,append 给切片是有点不寻常的。在其他语言中我很少见到这种情况。特别是,我在说 Ruby 和 C#,我想也是这样的。有时候我也见过有人 append 了数据,但没有重新赋值给切片,结果当然就出了问题。但正如你所说,David,这种情况其实非常罕见。
David Chase:可能只是因为它们一开始解释得很好。
Mat Ryer:是的,你学会了如何 append,所以你就这样做了,没问题。
David Chase:是的。
Mat Ryer:Yarden,我想回到你刚才说的……你提到了解这些内部机制让你成为了一个更好的程序员。我们如何控制什么放在栈上,什么放在堆上?因为在任何时候你都不会说“哦,放到栈上。” 你不会调用某个函数来实现这个。那么我们怎么知道东西会放在栈上还是堆上?我们如何控制它?
Yarden Laifenfeld: 这是个好问题。我想说 Go 里有一些神秘的黑魔法,我并不完全了解。我可以告诉你哪些肯定会放在栈上,比如当你创建一个局部变量时,它会放在栈上。或者当你把一个参数传递给函数时,它可能会放在栈上。David 刚才提到它可能会在寄存器里,但我觉得总体上来说,它不会放在堆上。所以我们应该这样想。
然后事情开始变得复杂的是,哪些东西不放在栈上,而是放在堆上。这通常是那些我们事先不知道会占用多少内存的东西。所以,如果我们想象一个普通的变量,比如整数或者浮点数,我们提前就知道它会占用多少内存,所以它会放在栈上。但如果我们创建一个映射、切片或者一个我们不知道有多少个元素的数组,我猜这些东西会放在堆上。我刚才说有一些魔法在起作用,这取决于你具体是怎么做的,但总体上是这个思路。
当我们谈到指针时,事情会变得有点复杂。如果我们把一个指针作为参数传递给函数---
有趣的是,垃圾回收器如何知道这个指针什么时候不再使用,或者它指向的数据什么时候可以释放?总体来说,思路是尽量把东西放在栈上,因为正如 David 所说,栈是临时存储。它会自动清理,不需要垃圾回收器。而且只有在你确实需要使用指针,或者确实需要在堆上存储东西时,你才会这么做,以此避免垃圾回收器运行的开销。
Mat Ryer:对。你说当东西在栈上时,它会自动清理……那么函数返回时会发生什么?因为假设这些参数会在调用函数时放在栈上。那么当这个函数返回时,会发生什么呢?
Yarden Laifenfeld:是的,理论上是会发生一些事情。当我们谈论栈时,我们通常会想到栈的末端有个指针,然后当我们从函数返回时,这个指针会移动……所以栈的结构是从高地址向低地址增长的,最后一块是最后调用的函数。如果我们在某个时刻看栈追踪,内存就是这样排列的:栈上会有最后一个函数的变量,然后是调用它的那个函数的变量,依次类推。
我们可以想象栈末端的指针在当前函数返回时移动到前一个函数的位置。这是理论上我们做了一些事情的地方。然而,实际上,除了那个指针,其他什么都没变。内存没有消失,它也没有被清零,下次我们写入相同的栈空间时,它只会被覆盖,我们基本上会认为它不存在了。所以栈并没有真正地增长和缩小,只是它的末端指针在移动。
Mat Ryer:是啊。我猜清零内存或者做其他操作会是额外的工作,所以没必要这样做,对吧?
Yarden Laifenfeld:对,没错。做最少的工作去实现你想要的东西。
David Chase: 人们不想要它。那样会变慢,而人们不喜欢变慢。加密领域的人曾经提出过类似的需求---
如果我在内存中写入了重要信息,我能多快将它清零?他们曾经问过这个问题,并且也有人提出过解决方案。我们还不知道什么是最好的方法。
Mat Ryer: 他们不能通过编程方式将它改为另一个值吗?
David Chase: 问题在于,编译器看到你对某些内容进行了许多写入操作,但实际上你并没有读取或使用这些内容,它会说:“我可以让这个过程更快。让我把这些写入操作去掉。” 这也告诉你正确的做法是,你需要添加另一段代码来验证你写入的内容确实被清零了。但这会花费更多时间。而加密领域的那些人会说:“等等,我们不希望花这么多时间。”
Mat Ryer: 是的……这样真的没办法让他们满意,对吧?
Yarden Laifenfeld: 是啊,总是那些加密领域的人……
David Chase: 没法让任何人满意。
Kris Brandow: 说到安全性---
那真的很慢……我听过《Changelog & Friends》 的一期节目,里面有 Oxide Computer 的人,他们说他们需要把打印出密钥的打印机拿出来,并在其微控制器上钻一个洞,以确保密钥永远不会被恢复。就安全而言,这是一整套复杂的过程。但这也说明了我们是如何看待计算的,大家总是在追求“速度、速度、速度”,而有时候我们可能应该在安全性和速度之间做一些权衡。我们只需要找到那个平衡点。
Mat Ryer: Kris,你觉得多快能把一台打印机砸坏?我觉得我能很快砸坏一台打印机,完全毁掉它。
Yarden Laifenfeld: 但这不是普通的打印机。必须是正确的那台打印机。
Kris Brandow: 而且你还必须以正确的方式砸它,因为你得确保所有的芯片、内存,甚至打印鼓---
如果是那种打印机---
都要彻底毁掉。
Mat Ryer: 像《终结者》一样。
Kris Brandow: 对,事情很多。
Mat Ryer: 把它扔进熔岩里。这是一个方法。把它放进熔岩里,然后慢慢融化。这个方法我从《终结者》系列里学到的。
Kris Brandow: 工业碎纸机。
David Chase: 而且这是一台打印机。它们从来没对我们好过。
Mat Ryer: 是啊,它们确实是我们不得不面对的最糟糕的东西之一,不是吗?虽然它们现在已经比以前好多了……但仍然是世界上最糟糕的东西,对吧?
Kris Brandow: 原始的物联网设备,也是我们发明的最糟糕的设备之一。
Mat Ryer: 对,我讨厌打印机。我不敢太大声说,怕我的打印机听到,下次就出问题了……
David Chase: 是的。
Mat Ryer: 以前经常遇到“你无法打印,因为打印机不在线”的情况。然后你去按个按钮让它“上线”,然后它又说“哦,稍等一下……” 这功能到底是干嘛的?为什么会有这种功能?为什么会有离线的选项?真的搞不懂,打印机……
Kris Brandow: 说到栈和堆,Yarden,你之前提到有时候我们想尽量把东西放在栈上,有时候则放在堆上……你有什么建议吗?比如什么时候该控制这些,什么时候该关注这些呢?
Yarden Laifenfeld: 你几乎总是应该把东西放在栈上,这意味着---
我自己的经验是,我在开始学习 Go 之前写了很长时间的 C。在 C 语言中,一个非常重要的做法是传递指针。这样做的原因是不需要复制不必要的大结构,把它们从一个地方传递到另一个地方。我想这是一种过去的做法,那时候硬盘容量很小,我们想节省内存---
但现在这不再是个大问题了;不过我就是这样被教的,所以我把这种做法带到了 Go 里。但其实这是错的,因为任何时候你可以复制一个结构体,也就是直接传递它而不是传递它的指针,你都应该这么做。因为这样一来,它可以被清理掉,而且你不需要给垃圾回收器额外的工作,去跟踪指针的引用,判断它指向哪里,是否还有其他指针。
所以不要为了节省内存而使用指针。这是我学到的一项重要内容。但你应该在需要共享某个东西的引用时使用指针。如果你遵循这个原则,你可能会获得一些性能提升,特别是如果你之前一直没有这么做的话。
Mat Ryer: 这确实有点反直觉。我见过一些刚开始学习 Go 的人,他们一学会指针,就想通过指针传递所有东西,因为他们觉得这样可以避免复制。甚至没有 C 语言经验的人也会本能地认为:“我只需要传递指针就好了,这样更简单。” 所以这确实很有趣。
David Chase: 抱歉,我刚刚在想这和调用约定(calling convention)有什么关系。
Mat Ryer: 什么是调用约定?
David Chase: 我们在使用寄存器(registers)的地方,我们是愿意使用大量寄存器的。
Mat Ryer: 什么是寄存器,David?
David Chase: 什么是寄存器……抽象地说,在现代处理器开始做各种疯狂的事情之前,你有一小部分暂存存储区,它们是固定大小的,并且有固定的名字。0、1、2、3,一直到有时是31,有时更多。你做的任何事情,最终都要将数据从内存中移入这些寄存器,然后在寄存器上进行操作,最后再存储回内存。抱歉,我不太确定现在它们是如何实现的……
Mat Ryer: 现在一切都被高度抽象化了。
David Chase: 是的,非常抽象。现在有了推测执行(speculative execution)和超线程(hyper-threading),寄存器的名字和实际寄存器之间有了一层间接层。尽管如此,与一台机器可能拥有的几千兆字节内存相比,寄存器的数量仍然非常少。现在,寄存器的数量从31或64可能增加到几百个,但仍然是一个固定的小数量。寄存器有固定的名字,而机器指令的大小也是固定的,有特殊的字段可以编码这些小数字,代表寄存器的名字。这就是寄存器的定义。我希望这能解释清楚……
你刚才提到指针或不使用指针的问题。Go 语言有一个机制叫做逃逸分析(escape analysis),Java 也有一点,而其他编程语言则较少使用这个机制。有时候你确实需要创建一个指向某个东西的指针,可能是因为你要调用一个别人写的函数,而这个函数恰好需要一个指针。或者你希望共享某个对象并对其进行修改,那么你就需要传递一个指针而不是整个结构体。Go 语言有一个特性,它的包导入没有循环依赖。这意味着你可以先编译运行时包,并且确定它不会依赖于其他任何东西。然后你可以逐层编译依赖于运行时包的其他包……
所以对于每个函数来说,你可以判断它是否保存了传递给它的指针。如果函数没有将指针存储到堆中或其他地方,那么这个指针就不会逃逸,你可以把它指向的东西留在栈上。这就是所谓的逃逸分析。对于每个函数和方法,Go 会生成逃逸分析的总结,帮助你知道某个指针是否逃逸到堆中,或者是否泄漏给了其他线程。这是一种让更多东西留在栈上的方法,而不是直接放到堆上。
Mat Ryer: 这种分析是在编译时发生的吗?还是在运行时?
David Chase: 目前这是在编译时发生的。我们一直在讨论如何做得更多、更好……我们内部有一些竞争的提案,大家来回讨论,看看哪种方法能带来最大的改进。这些改进是否会带来运行时开销?实现的风险有多大?是否会引发一些奇怪的 bug?有一种想法是,函数返回时它分配的内存不能放在它的栈上,因为当它返回时,栈就消失了,所以它必须放在堆上。问题在于,是否可以改变调用约定,让返回指针的函数使用某块特定的内存区域,因为调用者可能知道结果的生命周期,并且在使用完之后就不再需要它了。是否有办法将内存传递给调用者?
在 Java 中,有些实现通过硬件来实现这个功能,例如 Azul 公司开发的硬件,它会尝试将内存分配在栈上,但如果发现这是个坏主意,硬件会进行快速处理,并记录下来,提醒下次不要在栈上分配这个对象。而因为这是硬件实现,开销非常小,不像软件实现那样慢。那么问题就在于,在软件中我们能走多远?这是否值得?
Go 的垃圾回收器相比其他语言的垃圾回收器,比如 Java 的垃圾回收器,分配速度可能慢一些,回收垃圾的速度也慢一些。但它的优势是不会移动内存,能够处理内部指针,且拥有极小的全局暂停时间。所以这里有一些有趣的权衡。虽然使用指针的开销不大,但通过避免堆分配,你可能在某些情况下仍然可以获得性能收益。
Kris Brandow: 是的,我在编写代码时确实做过一些优化,确保我写的东西不会逃逸到堆中。我会有意识地写代码,确保逃逸分析能判断出这些内容会留在栈上。
我记得我们之前在工具相关的那期节目里提到过:就像代码覆盖率一样,我希望有一种分析工具可以在编辑器中把变量标记成不同的颜色,比如绿色或红色,告诉我哪些变量会逃逸到堆中,哪些会留在栈上。
如果有这样一种工具,可以更直观地显示编译器或者分析工具的判断,我觉得会非常有用。因为目前我们缺乏这样的工具,很多时候只能依靠直觉,然后事后分析代码,看看它们到底是在堆上还是栈上。所以如果能有一种更直观的方式展示这些信息,我觉得会非常有帮助。
David Chase: 这里有两个答案,至少有两个答案。我们曾经和一些对性能非常关注的 Go 用户进行过讨论,也和一些 IDE 开发者进行过交流。编译器有一个标志,可以输出一些信息,虽然对人来说有些麻烦,但它是为自动化工作流设计的。你可以使用这个标志,-json=0,目录名
,它会为所有编译的包生成 JSON 文件,记录编译器的所有失败信息,比如“对不起,无法去抖动检查。” 或者“对不起,不得不进行堆分配。” 这些信息都是以 LSP 格式(VS Code 使用的格式)输出的。虽然它有点复杂,但你可以把这些信息导入 IDE 中。不过问题是,编译器经常出错,它会不停地记录这些失败信息。但大多数时候这些错误并不重要,因为绝大部分时间你只是在一些关键地方遇到瓶颈。
所以你需要结合性能分析工具,专注于那些真正有问题的地方,而不是所有地方都看。否则你会觉得“编译器太糟糕了,看看它为我做了多少坏事。” 我们正在处理这个问题,PGO(性能指导优化)功能即将到来……但目前还只是实验性功能,未来会有更多更新。(译者注: 1.18已经有PGO功能)
Mat Ryer: 你提到这不是为人类设计的,因为信息太过繁杂……
David Chase: 是的,我觉得确实不是为人类设计的,信息太多。我认为需要一个过滤步骤,比如查看性能分析报告,专注于某个函数,看看它的坏消息是什么。
Mat Ryer: 那你会加个 CAPTCHA 验证吗?让人们点击“我是机器人”?这是一个方法。
David Chase: 不,我们没有这样做。
Kris Brandow: 我觉得这确实是个好主意。就像我之前提到的那样,如果这种工具能集成到 Gopls 或其他语言服务器中,那就太好了。这样我们就可以在需要优化的地方使用它,像运行覆盖率测试一样。我觉得这确实是个好方法,但你也不希望人们在某个只调用了一次的函数上过于纠结,试图让它避免堆分配。就像反射一样,有些人会担心在启动时使用反射会带来性能问题,但其实只要它只在启动时调用一次,完全没问题。
Mat Ryer: 这确实提出了一个很好的观点---
我们经常提到“先测量,再优化”。但这也引发了一个很好的问题,既然 Go 团队一直在幕后忙着进行优化和改进,那么是否有可能我们优化了代码,结果新版本的 Go 出来了,导致我们的优化变得多余,或者甚至使我们的代码性能下降?我们是否应该不断重新测量和评估?
David Chase: 我不会说永远不可能……
Mat Ryer: 你刚才说了两次“永远不可能”,David……
David Chase: 是的,我说了。我会说,永远不要说超过两次……
Mat Ryer: 已经四次了,继续吧。
David Chase: 好的……
Mat Ryer: 不好意思,我只是故意打断你。
David Chase: 对,我认为我们一个目标就是让你们过去的一些辛苦工作变得不再必要。我一直在想,“我们能不能重新排列字段,来让结构体更紧凑,而不用再告诉大家应该自己去手动排序字段呢?”这样我们就可以让优化指南更简短一些。然后你们之前辛苦调整字段顺序的工作---
抱歉,那可能是浪费时间了。
Yarden Laifenfeld: 如果有人真的在意字段的顺序的话……
Mat Ryer: 二进制编码时顺序确实很重要……
Yarden Laifenfeld: 是的,类似这样的情况。
David Chase: 顺便说一句,这不会很快实现,但只是一个想法。如果我们做到了,那就可以让优化步骤少一步。不过通常情况下,我们不会这么做。人们不喜欢他们的代码变慢。你可能还记得 Spectre 和 Meltdown 漏洞刚出来的时候……有时候安全补丁会让你的代码变慢。
有一次---
我记不清在哪里看到的---
有关于 Java 字符串的一个可怕问题,涉及到两种不同的编码方式。你可以传递它们到某个地方,可能会发生竞争状态。基本上,它验证数据,然后使用数据。但因为它是先验证再使用,所以可能有线程会竞争并在此期间破坏数据,导致数据不再有效。
Mat Ryer: 哦,如果你在这两个操作之间插手的话。
David Chase: 对,对,对。所以你可能会遇到非常奇怪的行为。我相信他们会修复这个问题,而修复的方法是必须添加一次拷贝操作。你会先复制它,然后验证,接着使用一个别人无法修改的版本,这样就增加了一些开销。Spectre 和 Meltdown 也是类似的情况,你觉得你的处理器很快很酷?结果我们要让它变慢一点,而且你还得生成不同的代码。如果这是你的问题,那就只能接受它变慢了。
当你第一次听说 Spectre 和 Meltdown,还有 Rowhammer,你会觉得“你做了什么?天呐!” 因为你希望硬件能正常运作,而不是给你带来这些麻烦。
Kris Brandow: 是啊。我想回到我们之前讨论的结构体字段重新排列问题,有时候按顺序排列字段让它们更紧凑,确实不错,可以节省空间。但由于缓存的工作机制,有时候你反而希望它们不要紧挨在一起,你会想“不要,我希望这些东西在不同的缓存行上。” 如果它们在同一缓存行上,我的性能就会大大下降。所以你可以看到,自动为用户解决这个问题会有多么复杂。
David Chase: 但这不是 Go 101 的内容,我也不认为我们会自动解决这个问题。
Kris Brandow: 是的。
David Chase: 我是说,这也不是你会教给初学 Go 程序员的东西。
Kris Brandow: 对,我只是说如果你重新排列结构体的字段,可能会把某些东西放在不该挨在一起的缓存行上,导致性能问题。
David Chase: 你还得有一种方式来规定与其他编程语言或操作系统的交互。事情必须按某种方式排列,如果不是,那就太糟了。
Kris Brandow: 对。
Mat Ryer: 说到这儿,我们到了该分享不受欢迎观点的环节了。
Mat Ryer: 好了,Yarden,你有什么不受欢迎的观点吗?
Yarden Laifenfeld: 有的。我认为这个观点真的很不受欢迎。我的观点是,Go 不应该再增加任何大功能。
Mat Ryer: 哦,为什么呢?
Yarden Laifenfeld: 我喜欢简单。我喜欢简洁。你之前问我最喜欢的编程语言是什么,我说了 C 和 Go。我认为原因是,当我看 Go 代码或 C 代码时,我知道它们在做什么。而如果我看 Java 代码,如果有人使用了不同的编码规范,或者使用了我不熟悉的新功能,那么这段代码对我来说可能几乎是不可读的。而我觉得 Go 在保持简单这一点上做得非常好。我喜欢它真的强制你遵循某种结构,而不像 Python,那几乎让你可以随意编写代码……甚至 Go 的静态检查工具也会告诉你“这不是 Go 中命名变量的正确方式。” 我非常喜欢这一点。我觉得这真的有助于快速入门,帮助阅读他人的代码,也帮助写出好的代码……因为你只需要学习基础知识,然后在此基础上构建,而不是不断学习新的东西,这样你会比不断学习新东西更快地成为一个更好的开发者。
所以总的来说,如果我们增加任何新的大功能,我们就会偏离这个理念。而且我认为大多数大功能都会违背这个理念,不会给语言用户带来太多价值。当然,我不是 Go 团队的一员,我也没有数据支持我的观点。
Mat Ryer: 是啊。
Yarden Laifenfeld: 这不是基于统计数据的。
Mat Ryer: 你这是在让 David 失业啊。但让我问你这个问题……那泛型呢?你对它有什么看法?
Yarden Laifenfeld: 我想你应该能猜到我对泛型的看法了,Mat……[笑声] 我真的很喜欢只有一种方式可以做事。我知道……嗯,我知道我们已经作为开发者成长了,我希望我们能回到过去……开玩笑的。但我确实喜欢我们在进步,喜欢事情变得更抽象化,但我也喜欢 Go 让你始终保持接近事物本质。你确实需要知道指针是什么……如果你听了这个播客,你就知道栈是什么……这些重要的东西。所以---
天啊,我完全忘记我刚才说到哪儿了。
Mat Ryer: 没关系。那么,David,你现在有没有考虑增加一些大功能呢?你怎么看这个观点?
David Chase: 我对泛型的接受度很感兴趣……我们在讨论扩展迭代器,使它们更通用。而且有人在讨论协程,一旦协程不是---
协程不会是 Goroutine,因为用 Goroutine 实现协程速度不够快。
Kris Brandow: 我确实觉得协程是我想要的一个功能。
Yarden Laifenfeld: 问题是,这些都是我想要的功能。比如,我以前是非常想要泛型的。在泛型没有出现之前,我一直在想“天啊,我需要泛型。” 但由于我们需要支持非常古老的版本,我还没有机会用它们……我心情矛盾,但这仍然是我的不受欢迎的观点。我坚持这个想法。
Mat Ryer: 很好。David?
David Chase: 我有好几个不受欢迎的观点。我不认为它们比“不要增加新功能”更好,但……
Mat Ryer: 这已经很不受欢迎了。
Yarden Laifenfeld: 不,不是不要增加新功能。顺便说一句,我认为标准库可以增加一些新功能……
David Chase: 啊……[笑]
Yarden Laifenfeld: 我觉得增加一些像“max”这样的功能不算坏。
Mat Ryer: Yarden,这不是对 David 的绩效评估啊。[笑]
Yarden Laifenfeld: 不,不好意思……我只是想说,我感到非常抱歉。我真的非常抱歉。请继续开发 Go。你们做得很好。
David Chase: 我不确定……我的其中一个不受欢迎的观点可能更多是 Go 团队内部不受欢迎的,挺技术性的。多年前,我与 Fortran 有很大关联。我的导师被称为“Fortran博士”。
Mat Ryer: 听起来像是个反派。
David Chase: 我曾在 John Backus 先生手下实习。 (译者注: 约翰·巴科斯,FORTRAN开发小组组长,提出了巴科斯范式,1977年图灵奖得主)
Mat Ryer: 哇哦。
David Chase: 是啊,挺酷的。
Mat Ryer: 这确实很酷。
David Chase: 所以我对 Fortran 有一种特殊的情感。让 Fortran 快的关键在于一个非常小的东西,而大多数程序中这通常是正确的。那就是,当你传递一对切片给一个函数时,Fortran 规定你不能假设它们重叠。如果它们重叠,那就不是 Fortran 了。而这是一个有趣的规则,无法通过语法检查,但如果你的代码传递重叠的内存给函数,而它能检测到,那么它就不符合 Fortran 规范。因此,这是处理所有 bug 报告的一个便利方式。如果你的代码能检测到这种情况,它会说:“这没问题,但这不是 Fortran 的问题,这是其他语言的问题,自己想办法吧。”
但这样做的好处是,它可以让你毫无顾忌地进行向量化(Vectorization)、并行化、以及重新排序等各种优化。这几乎是 Fortran 快速的关键之一。因此,我有时候会想---
有很多关于不同语言之间调用和数据转换的烦恼,这从来都不有趣,总是令人烦恼。
所以,如果你说“我们可以让 Go 代码变快”,如果你做了这个---
这需要大量的编译器工作,并且对像我这样的人来说意味着更多的工作机会,这很好……但你可以通过修改参数规则来让代码更快。为了让这从上个世纪或上个千年走出来,我会说:“机器学习!哇哦!” [笑] 所以这就是我的不受欢迎的观点,可能在 Go 团队内部更不受欢迎。
Mat Ryer: 他们大概能明白你的意思。[笑]
David Chase: 是的。我担心这就是问题所在。另一个问题是持有不太受欢迎的观点,而人们并不完全理解。
Mat Ryer: 某种程度上,这也挺聪明的。David,你还写下了另一个观点,我挺喜欢的,就是Go语言需要更大的整数类型。int128
、int256
、int512
... David,你打算用这些巨大的整数干嘛?
David Chase: 现在的处理器都有这些疯狂的额外指令,它们需要巨大的输入和操作数。而在C语言中,这些被编码得很糟糕---
出于某种原因,他们为C语言的整数类型选择了愚蠢的名字。当他们需要使用这些大整数类型时,又不得不选择更愚蠢的名字。你用它们的时候,还得包含一个特殊的头文件,然后污染你的代码,充斥着这些糟糕的名字。而Go语言可以将这些指令作为内置函数,并且Go可以为这些输入类型起一个完全合理的名字:int128
、int256
、int512
。我认为这很不错。我们可以实现这一点。我们已经在32位机器上处理64位整数了,这在编译器中并不难。因为有人问我们:“我们真的想用这些内置函数,我们想这么做”,于是我们不断讨论最佳方式,但我们没有合适的类型,所以我们只能用结构体做这种很hack的事情,但那个结构体是特殊的... 不如直接说:int128
,搞定。
Kris Brandow: 这让我想起了---
我记得是罗布·派克(Rob Pike)提过一个提案,想把int
和uint
改为任意精度整数……我当时想:“我喜欢这个。我想在我的语言中直接使用任意精度。” 但…
David Chase: 这可行。我觉得有趣的是,是否可以研究一种默认行为,或者让你可以选择是否编译代码,使得如果溢出---
一个反提案是,如果有符号整数溢出,程序直接崩溃;这是个小小的安全问题,但对Go来说可能没那么严重。确实有一些利用整数溢出的漏洞,但这些漏洞还利用了“哈哈!那些人没有检查他们的数组边界。他们以为检查了数组边界,但我们让整数溢出了。我们可以玩了。” 而Go会直接说:“不,我们检查了你的数组边界,走开吧。”
Kris Brandow: 对。
David Chase: 这可能不必要,但这是一个反提案。我认为这些大整数可能不会放在栈上,嗯,也许有时候会放在栈上,但它们必须有一个选项让它们不放在栈上。
Kris Brandow: 说得对,是的。
Mat Ryer:好的,那我们就以这个话题为结尾吧---
谢谢你,David,把我们拉回到栈这个话题,并且为今天的讨论画上了一个完美的句号……非常感谢你。Yarden,非常感谢你为社区做的所有工作。以色列的GopherCon,如果有人在那片区域,想要去见见Go语言的开发者们,那就赶紧加入吧……还有“Women Who Go Israel”组织……Go语言社区的美妙之处就在这里,像你这样的人投入了大量的工作。如果你不亲自参与,你可能根本不知道这需要多少工作。我只是在旁边看了一眼就知道人们付出了多少努力,所以真的非常感谢你。
David Chase---
显然,你在Go团队上的所有工作……我们还能说什么呢?非常感谢你做的这一切。Kris,我没什么好感谢你的,除了你能出现并做你自己之外。还有谢谢我们的听众们,感谢你们的收听……因为说实话,如果没有你们,这一切真的都没有意义。我是Mat Ryer,我也要感谢我自己……没人会这么做,那我自己说吧。非常感谢大家,我们下次在Go Time再见
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。