Mat、Johnny、Jon 和特邀嘉宾 Ian Lance Taylor[1] 讨论了 Go 中的泛型。什么是泛型?为什么它们有用?为什么接口不够用?如果将泛型添加到 Go 中,标准库将如何变化?社区对泛型有何贡献?如果添加泛型,这将对语言产生什么负面影响?
本篇内容是根据2019年8月份#98 Generics in Go[2]音频录制内容的整理与翻译
过程中为符合中文惯用表达有适当删改, 版权归原作者所有.
Mat Ryer: 大家好,欢迎来到 Go Time!我是 Mat Ryer[3],今天我们要讨论的是“泛型”,这是一个既有趣又有时颇具争议的话题。今天和我一起的有 Jon Calhoun[4]……你好,Jon。
Jon Calhoun: 嗨,Mat!
Mat Ryer: 还有 Johnny Boursiquot[5]……
Johnny Boursiquot: 大家好。
Mat Ryer: 以及唯一的 Ian Lance Taylor。你好,Ian。
Ian Lance Taylor: 你好!谢谢你们邀请我。
Mat Ryer: 是啊,谢谢你加入我们。这太让人兴奋了。我们都在 GopherCon 上看到你演讲,实际上今天视频已经发布了。所以如果有人还没看过 Ian 在 GopherCon 上关于这个话题的演讲,现在你可以去看 这个视频[6]。
Jon Calhoun: 不是现在……大概几个小时后。
Mat Ryer: 说得好,谢谢 Jon。[笑] 或许我们可以先从泛型是什么,及其对 Go 的意义开始聊聊。为什么这是一个我们不断讨论、不断听到的话题,尤其是那些从其他编程语言看 Go 的人。
谁想先来解释一下泛型,供不太熟悉这个概念的听众了解?
Ian Lance Taylor: 我很乐意试一试。泛型是一种编程方式,你在编写代码时不指定值的确切类型,而是使用类型参数代替实际类型。然后在实际构建和执行程序时,才选择这些类型。这样你可以编写一套算法和数据结构,它们可以独立于实际类型而工作。
Mat Ryer: 说得很好。但这和空接口不同,对吧?泛型依然会在编译时进行类型检查。
Ian Lance Taylor: 是的,没错。这仍然是在编译时基于类型参数进行静态检查。
Jon Calhoun: 那么你会说,映射(map)和切片(slice)在某种程度上也是泛型的一个例子吗?
Ian Lance Taylor: 是的,映射和切片本质上都是泛型类型。它们是内置在语言中的泛型。所以当人们谈论 Go 中的泛型时,可以说他们想要自己编写类似映射和切片的泛型数据结构和算法,而不是直接使用内置的映射和切片。
Johnny Boursiquot: 对于一些人来说,没有泛型会带来一些痛苦---
或者说人们认为这是痛苦的。因为如果没有泛型,你要么依赖空接口并进行类型转换,这本身带来了风险;要么你可以生成代码……但问题是,为什么我们需要泛型?其他编程语言有泛型,Go 则一开始就没有采用这种编程方式。你怎么看待当初为什么没有引入泛型,现在为什么又要重新考虑这个问题?
Ian Lance Taylor: 当时没有引入泛型的原因是它很复杂。你需要非常仔细地考虑如何进行类型检查,如何让程序工作。定义类型参数和类型实参本身也很复杂。
现在,我希望我们通过设计草案将复杂性最小化,但不可否认的是,它会为语言增加一些新概念。不过话说回来,人们希望在 Go 中看到泛型是有原因的---
正如你所说,人们熟悉其他语言中的泛型。在 Go 中,有一些代码类型我们无法编写,因为我们没有泛型。虽然没有泛型我们依然可以写出大量优秀的代码,但如果我们有了泛型,我们可以编写一些现在无法实现的库。例如,一个典型的例子是 ConcurrentHashMap,一个可以在多个 goroutine 同时安全修改的哈希映射,并且像标准语言中的映射类型一样是类型安全的。
另一个例子是一些现在无法编写的算法,比如适用于任意类型通道的算法。你可以编写简单的函数,将两个通道合并,或者将一个通道多路复用到多个其他通道中,或者进行其他你想做的事情。目前,你需要为每种类型单独编写,因为你无法表示“我有一个通道,但我不关心通道的类型。”你总是必须说“我有一个 chan int
”或“我有一个 chan struct
”,而不能只说“我有一个通道,我还想在它上面写一个 select 语句。”这是目前在 Go 中很难实现的。
Jon Calhoun: 我记得---
是上周吗,Mat?当时你们在讨论 io.Writer 和 io.Reader 接口……
Mat Ryer: 是的。
Jon Calhoun: 这让我想起了这个。我们可以围绕这些流行的接口编写很多很酷的代码,而且我们并不关心我们从哪里读取……听起来你在说,关于通道,我们也可以做类似的事情。但遗憾的是,我们目前无法做到这一点,尽管我们本可以围绕通道构建出许多通用功能。
Ian Lance Taylor: 是的,没错。所以泛型可以帮助 Go 程序员的一个方式是---
正如你所说,你可以编写非常强大的接口,但你必须编写实现这些接口的方法。你可以从概念上看作 Go 中的所有内置类型都有它们自己的方法。它们不是以方法的形式编写的,而是像加号(+)或通道的接收和发送操作符这样的运算符。但目前我们无法将这些概念捕捉到接口中,而泛型可以让我们做到这一点。
但泛型不仅仅是接口。你还可以编写描述多个类型之间关系的泛型。你不必总是处理单一类型。例如,典型的例子是图(graph),其节点和边(node 和 edge)类型是不同的。你可以编写通用的图算法,这些算法适用于实现图算法要求的类型,但你不必指定这些类型究竟是什么。
Mat Ryer: 在这种情况下---
我在你的演讲中看到了这个例子,Ian---
如果你有一个图和一个节点,并且你有一个同时包含两者的契约(contract),那么这个契约只有在你为它们提供了类型时才有意义,对吗?这些类型基本上是必须的。
Ian Lance Taylor: 是的。在演讲中的图例子中---
是的,每次你想要处理一个图时,你都必须提供两个类型参数,一个描述节点类型,一个描述边类型。
Mat Ryer: 这很合理。当然,如果你忘记了其中一个类型,编译器在那时就会帮助你。
Ian Lance Taylor: 是的。这就像你调用一个函数时没有传递足够的参数一样。
Jon Calhoun: 当我们谈论这些不同的数据结构时,我有个问题:你觉得因为泛型的引入,标准库会变得更大吗?
Ian Lance Taylor: 这很难说。我不觉得标准库会变得非常大。我会说,我预计会看到一个新的 chans
包,比如包含我提到的通道算法。同样,也会有一个新的 slices
包,里面包含适用于任何类型切片的一些简单切片算法。除此之外,很难说。
我认为人们将能够编写适用于不同方式的泛型数据结构,但大多数这些数据结构会存在于标准库之外。我认为只有当我们看到这些结构有明确的用例时,才会考虑将它们引入标准库。所以我不认为标准库会一下子变得非常大,但当然,仍然可以根据需要添加一些新的具有普遍适用性的东西。
Jon Calhoun: 这在 Go 2 的讨论中经常出现;虽然泛型可能不需要 Go 2 才能实现,但大家似乎都在这个背景下讨论泛型。我猜其中一个好处是标准库中的某些现有包可能会因为泛型的引入而改变。我想象,像 Sort
包可能会有变化。你觉得这是对的吗?
Ian Lance Taylor: 是的,我同意。Sort
包可能会改变,Container List
包和 Hash
包也会。我们可能会保留旧的包,但很可能会有新的版本使用泛型设施。
Mat Ryer: 但我们不会有新的切片类型,对吧?
Ian Lance Taylor: 不会。
Mat Ryer: 切片类型应该会保持不变,是吧?
Ian Lance Taylor: 是的,切片已经很好了。没有理由改变它们。
Mat Ryer: 它们确实很好。
Jon Calhoun: 我觉得 Sort
包很好用,特别是当你掌握它后。但如果你习惯了其他语言的方式,初次接触 Sort
包时,有时会觉得有点困惑。虽然它已经越来越好,但我确实觉得泛型可以让 Go 变得更易用。
Mat Ryer: 其实,Sort
包展示了如何通过现有 Go 代码实现类似泛型行为的一个很酷的例子。你可以传递一个函数,并依赖闭包访问数据;你只需要比较两个元素的索引。这有点像个技巧,但确实有效。当然,泛型远远超越了这一点。
Ian,你之前说的一点让我深有共鸣,你提到 Go 中没有泛型是因为它很复杂。我觉得对开发人员和工程师来说,这个概念非常合理;但对于从事产品工作的人来说,他们通常不理解这一点……所以听到这个观点很不错。而且我也很高兴看到你们在 Go 团队中能够基于技术现实和“机械同情”(mechanical sympathy)做出这样的决策。
Ian Lance Taylor: 是的……我认为 Go 成功的很大一部分原因在于它的简单性。当你编写程序时,如果你花费几分钟或几个小时来决定应该使用哪种语言结构(在某些其他语言中),那么那不是生产性的时间。你希望语言足够强大,能够完成所有任务,但又不至于太复杂。你不希望纠结于语言的某些方面究竟是如何工作的。
所以如果我们最终在 Go 中添加了泛型,我们必须保留这种特性。这是语言中最重要的特性。
Mat Ryer: 是的,还有可读性。我经常强调这一点---
编写代码时更多是为了使用 API 和阅读代码,而不是为了编写本身。这也是为什么我个人不介意总是写 if err != nil
。我已经非常习惯了,因为当我阅读代码时(我做得更多的是阅读而不是编写),它非常清晰,表达非常明确。
这是我喜欢最新泛型提案的一点。如果你看代码,它依然看起来像 Go……尽管现在有了额外的一组括号需要考虑。
Ian Lance Taylor: 很高兴听到你这么说,因为这是我们真正追求的目标之一。它应该仍然看起来像 Go。
Mat Ryer: 是的,我觉得这是一个很好的目标。这也是我反对 Try
提案的原因之一。我稍微跑题了……我觉得 Try
提案有点像魔法,不太符合我对 Go 的直观认识。而最新的泛型提案,我觉得它仍然保留了 Go 的风格,如果可以这么说的话……
Ian Lance Taylor: [笑] 很好。
Jon Calhoun: 在你们设计这个功能时,为了让它易读且易用,我猜测你们也参考了其他语言,寻找灵感,看看哪些方式行得通,哪些不行……你能谈谈这个过程吗?
Ian Lance Taylor: 当然。很明显---
也许并不那么明显,但事实是我们最熟悉的语言是 C++,所以我们花了大量时间研究 C++ 中的泛型实现,当然,在 C++ 里它被称为模板(templates)。我们很清楚,C++ 模板有些方面很难引入到 Go 语言中,甚至我们根本不想让它们进入 Go。你可以把 C++ 的模板看作是另一种编程语言,实际上它是图灵完备的,像是叠加在普通 C++ 语言上的一层,只不过它使用的是完全不同的语法,并且在编译时执行。
这就是人们所说的模板元编程(template metaprogramming)。你实际上可以用模板语言编写整个程序,但这些程序非常难以理解……但这不是我们想要走的方向。我们希望剔除这些复杂性,只保留核心思想,让人们能够使用类型。
当然,我们也研究了 C++ 的语法,很多人熟悉使用尖括号(angle brackets)的方式,但我们无法找到在 Go 中让它工作的方法……因为在 Go 中,你可以在不知道名字的具体类型的情况下解析语法;虽然为了完全解析程序你需要知道类型,但你实际上可以在不知道类型的情况下完成所有解析,而 C++ 并非如此。在解析 C++ 时,你需要知道某个东西是模板还是普通变量。而我们需要保留 Go 语言中轻松解析的能力,这让编译速度更快,也让很多重要工具,比如 goimports
更容易解析代码,因为它们不需要理解每个名字的类型。
这是我们开始时的情况……当然,我们还参考了很多其他语言,比如 D、Ada、CLU……CLU 早在 70 年代就有了很多泛型的思想。很遗憾,这门语言没有继续发展下去。当然,我们也参考了 Java。
Mat Ryer: 我喜欢的一点是,在某些情况下,作为用户,你可能并不需要知道泛型的存在。比如有些例子中,可以从你传入的参数推断出类型。因此,在这些情况下,它看起来就像在调用一个普通的 Go 函数,这一点我非常喜欢。
Ian Lance Taylor: 没错。类型推导实际上是我们花了大量时间研究的内容。因为一方面,我们非常清楚自己想要它,正如你所说,用户可以调用一个泛型函数,甚至不一定意识到它是个泛型函数。但我们也必须制定不会让人感到意外的类型推导规则,这也是我们从 C++ 中了解到的。C++ 还有函数重载和类型推导,这些非常复杂,有时确实会让人感到意外。因此,我们花了很长时间写下了一套足够简单的规则,适用于大部分情况……至少我们希望它们足够简单,适用于大多数情况。毕竟,迄今为止还没有太多泛型代码被编写出来,因为还没有完整的实现。
Jon Calhoun: 那么,当你们考虑不同的方案时,会写部分实现来试验一下吗?
Ian Lance Taylor: 是的。
Jon Calhoun: 我知道现在(我想)有部分实现,但我不确定你们是否会为每个方案都写部分实现。
Ian Lance Taylor: 是的。我们为许多不同的方案写了部分实现,这确实帮助我们发现了解析问题。我们会在解析器中实现它,并用一些测试用例来试验;或者仅仅在解析器中写代码时,我们就会遇到一些情况,发现“哇,我们完全不知道该如何解析这段代码。”这帮助我们逐步走向今天建议的这种相对简单的语法。
Mat Ryer: 这是一个非常有趣的方式……因为你需要解析这门语言,所以解析成为了一个主要关注点。这并不是我们从外部设计泛型时会想到的事情;我们总觉得“设计什么都可以,任何我们能在记事本中输入的东西都可以。”但实际上,你还得考虑现有工具和解析器等的兼容性。我看到 Contracts 已经在解析器中实现了,我想有个 PR。
Ian Lance Taylor: 没错,Robert Griesemer 已经写了一个解析器,并且完成了大部分类型检查器的工作。这项工作进展得非常顺利。
Mat Ryer: 这真令人兴奋。
Johnny Boursiquot: 除了这个概念被引入语言中,contract
是唯一一个新增的非常显眼的关键词吗?这是开发者会意识到泛型功能的第一步吗?
Ian Lance Taylor: 是的,没错。一个新的关键词---``contract
。在当前的设计中,这就是我们唯一新增的内容。你说的对,这是开发者最先看到的东西……但实际上我认为大多数人不会首先使用 contract
。虽然 contract
是我们设计中的关键元素,但你实际上可以在没有 contract
的情况下编写很多泛型代码。我认为我们确实需要 contract
,但你可以不依赖它,直接使用类型参数和类型实参来写很多代码。
Mat Ryer: 你的意思是使用现有的 contract
吗?内置的那些?
Ian Lance Taylor: 不,我指的是完全不使用 contract
来写代码。比如我提到的通道算法。你可以使用类型为 T
的通道写很多东西。你根本不关心 T
的具体类型,所以不需要 contract
。
Mat Ryer: 我明白了,任何类型都可以传入。
Ian Lance Taylor: 没错,完全正确。
Mat Ryer: 我觉得,一旦泛型可用,很多经典问题会立即得到解决,尤其是在标准库中解决。接下来我想讨论的是,首先,社区如何为此做出贡献?其次,我很感兴趣的是---
如何避免我们每个人都去建立自己的库,如何避免每个人都各自实现一遍通用功能?如何围绕一个中心化的地方来集合这些资源?希望我们需要的所有东西,比如集合、图结构、树等等都能共存在一个地方……你觉得这些会进入标准库,还是会先在社区中出现?
Ian Lance Taylor: 这些问题很棒,但我不知道最终会如何发展。我希望很多东西会先在外部实现,然后再进入标准库。但正如你所说,有些非常明显的东西,比如集合,似乎从一开始就应该添加进去。我现在还不确定情况会怎样发展。
Mat Ryer: 是的,很有趣。比如 sync.Map
是你提到的一个例子,如果能直接有一个带类型的 sync.Map
就很棒了。
Ian Lance Taylor: 没错。
Mat Ryer: 因为当你开始学习 Go 时,通常并发是你非常想要尝试的东西,因为语言原生支持这些并发原语……我知道很多人对 Go 的这部分非常感兴趣。所以如果能以一种直观、简单的方式使用 sync.Map
,这无疑会帮助很多新开发者。而且我觉得这个功能大多会吸引那些更高级、更有经验的开发者。初级开发者可能一开始会避开它。我不知道你们怎么看这个问题……
Jon Calhoun: 至少在我看来,我希望这是一个你不需要强制使用的东西。如果你不编写提供泛型实现的库,而是像现在一样使用映射和切片,你甚至不需要考虑它们是泛型的。我希望这种情况能够让初级开发者不会一开始就被吓跑。
Mat Ryer: 是的。我觉得现在编程中常见的一个错误---
我自己也犯过这种错误---
就是过早抽象化。当我看到一个概念时,我很容易立即想把它抽象出来,但现在我会尽量多实现几次,看看是否真的需要抽象。泛型的强大功能可能会让人们更早地去构建抽象,这很诱人。这是我们作为社区需要讨论的一个问题。说到社区……你们对此有什么想法吗?
Ian Lance Taylor: 我完全同意你说的。新事物确实容易被过度使用。我觉得在 Go 的早期,通道也是这样。我们花了一段时间才真正理解通道的用处,以及它们在哪些情况下带来了过多的复杂性,或者过早的抽象。我们需要尝试学习,并且希望能建立一个良好且简单的基础,以便我们在其基础上继续学习。
Mat Ryer: 是的,完全同意。我是早期滥用通道的那类人。我在很多地方使用它们,实际上根本不需要……现在我通常是从互斥锁(mutex)开始,有时候甚至不需要进一步扩展。但我记得那段时间……我们现在理所当然地认为可以轻松启动 goroutine,让它们安全地通信,并使用这些语言原语实现这一切,这非常强大……所以我可以理解为什么人们会兴奋并想要使用它。
Ian Lance Taylor: 是的。
Mat Ryer: 说到社区,社区在泛型中的贡献有多大?我知道每当你们讨论任何特性或语言改动时,都会引发大量讨论……我认为这也体现了 Go 团队的一些核心价值观,比如简洁性。社区似乎对变化有一定的抵触情绪……你觉得社区的参与度如何?是否达到了预期?我们可以如何改进?你对此有何感受?
Ian Lance Taylor: 我认为社区的表现非常好。多年来,我们从社区中获得了很多想法。实际上,泛型的讨论自 Go 诞生以来就一直持续。很多人贡献了非常有趣且有用的想法……当然,也有人说“不要泛型,无法接受,太复杂了”,我尊重这种观点。当然,我现在讨论的是泛型,但目前还不能保证它们一定会进入语言;我希望它们能进入。
多年来,关于泛型的讨论有很多不同的观点和想法,我认为这些讨论帮助我们了解了如何解决这个问题,以及我们可以做些什么。它还帮助我们看到了那些本可以通过泛型更容易解决的代码实例……从而确保我们提出的设计足够强大,能够解决这些问题。
因此,最有用的东西就是那些泛型能够帮助解决的实例,这些实例确保我们提出的设计确实有用。社区还对语法和语义提出了很多好的想法……虽然有些想法过于复杂,但它们帮助我们在核心功能和强大性之间找到了平衡,这将使泛型成为语言的一个有用补充。
Jon Calhoun: 我记得大概在过去一两年---
也许更早一些---
我看到有一个 Google 团队的成员举了一个例子,说明使用空接口(empty interface)实际上造成了性能问题,而泛型可以帮助解决。但似乎我们花了很长时间才看到这种真实的例子。这是对的吗,还是我遗漏了什么?
Ian Lance Taylor: 不,你说得很对。这类问题的确需要时间来理解。理解任何语言都需要时间,理解空接口的性能影响也需要时间……所以你说的没错。
Jon Calhoun: 那你认为这是否是我们现在更加关注泛型的原因之一?我感觉泛型现在变得更加重要,部分原因可能是我们现在看到了它的实际用例,而过去它只是“我们希望有,但还不够重要”。
Ian Lance Taylor: 我认为你的看法有一定道理……但我认为我们现在更加关注泛型的另一个原因是,我们终于觉得自己可以掌控这个问题了。当然,大多数人已经很开心地使用 Go 很多年了;我自己也思考泛型问题很多年了,早期的一些提案已经发布,但它们都很糟糕……还有一些没有发布的提案,甚至更糟糕。
有几份未发布的提案,我只是写了出来,自己思考了一下,然后只分享给了几个人,比如 Robert 和 Russ,他们会告诉我,“这个提案很糟糕。”我认为,经过这么多人的帮助,我们终于达到了一个阶段,人们的第一反应不再是“这很糟”,而是“嗯,也许我们可以让它变得有用。”
Johnny Boursiquot: 鉴于泛型引入带来的复杂性问题,你认为当前的提案是否达到了不引入过多复杂性、不给用户增加过多负担的标准,从而保持 Go 的简洁性?我们都非常努力地想要避免一些复杂性进入 Go,你觉得现在的提案达到了这个标准吗?
Ian Lance Taylor: 我觉得你问到了核心问题。我们需要作为一个整体社区来回答这个问题。就我个人而言---
是的,我认为我们已经达到了这个平衡。但我不是最终的决策者。我们需要达到一个阶段,让大家能够尝试这个实现,然后看看大家的反馈。
Jon Calhoun: 实现是这里的关键,因为仅仅看提案,你会觉得“嗯,这看起来不错”,但有些东西在你真正深入使用之前,你很难知道它到底会是什么样的感觉,是否直观。因为有些东西从外部看起来很简单,但实际上并不是,而有些东西看起来复杂,但当你使用时,你会发现“哦,这其实很简单。”
Ian Lance Taylor: 是的,我同意。
Mat Ryer: 我们的社区 Slack 频道非常热闹。大家正在实时收听,提问... Marwan 问道:“预计 Go 的编译速度会变慢多少?对此有没有相关的目标?” Dylan Barack 随后补充说,如果编译速度只慢 50% 到 100% 之间,他还能接受,Ian,你觉得这可能吗?
Ian Lance Taylor: [笑] 首先,不使用泛型的 Go 编译速度应该完全不会受到影响。其次,我想说的是---
Mat Ryer: 哦,抱歉,Ian,你指的是编写泛型代码,还是甚至在使用泛型代码时也不会受到影响?
Ian Lance Taylor: 我是说即使在使用泛型代码时也不会受到影响。
Mat Ryer: 好的。
Ian Lance Taylor: 现有的 Go 程序显然没有使用任何泛型代码。所以现有的 Go 程序不会因为语言中添加了泛型而变得更慢。但当前的设计实际上设想了几种不同的编译策略,我们预计如果泛型真的被添加到语言中,我们将不得不通过实验来让编译器根据不同的情况选择不同的策略。
其中一种策略是慢速版本,在这种情况下我们会为每个步骤或类型参数重新编译所有内容。但我认为在大多数情况下没有必要使用这种策略。然后还有一种策略,是类似于我们今天实现接口的方式,但并不完全相同,因为我们不希望有接口那样的分配需求,但仍然是基于类的类型参数来编译。
你知道,最简单的层面上,我们可以根据每种类型有多少指针来描述类型。所以你可以根据不同的指针集为每个泛型函数重新编译。如果你用非常大的类型参数实例化---
是的,或许你会为此做一个特殊处理,但这种情况不会经常发生。因此,在这种情况下,你可能会为每个泛型函数编译四次或八次。但这并不意味着你的编译速度会慢八倍,因为大多数函数并不是泛型函数。这只是我们可以使用的几种编译策略之一。
我觉得如果编译器变慢 100%,那将是一个失败。我们不希望编译速度变得那么慢。如果普通程序的编译时间真的增加那么多,我们可能无法推行泛型。当然,你肯定可以写出让编译器变得非常慢的极端程序,但普通情况---
Mat Ryer: 那种我会写的极端程序。
Ian Lance Taylor: [笑] 普通情况下,编译速度不应该变慢 100%。我希望速度只会慢 25% 左右;不过这是我随便说的,因为我们还没有进入真正的实现阶段。
Mat Ryer: 好的,我们不会因此抓住你不放,Ian... 但这确实很有趣。听到你们在为语言添加新特性时要考虑的所有不同因素,感觉很有意思。正如我之前说的,从外部来看,我常常只认为这是语法上的改变,但实际上还有很多其他方面要考虑。我也很好奇,Nathan Youngman 在 Slack 上问道---
他没有说明自己多大年纪---
在接口和泛型之间是否可能会有一些编译器优化?如果我们最终拥有运行速度更快或性能更好的东西?这是个有趣的想法。
Ian Lance Taylor: 是的,这是个有趣的想法。我以前没有想过这个问题,我也不知道。
Mat Ryer: 很好,我也不知道。
Jon Calhoun: 关于编译时间的讨论非常有意思,正如 Mat 说的,因为有些方面我从未考虑过。我平时不做那些编译时间特别关键的项目,所以即使让我的编译时间延长 10 倍也没啥关系。但有些人肯定不是这种情况... 我可以想象,实施这些新特性并引入新的功能肯定会很复杂。
Mat Ryer: Jon,你会写单元测试吗?
Jon Calhoun: 会啊。
Mat Ryer: 那如果让你的编译速度延长 10 倍呢...
Jon Calhoun: 是的,如果编译变慢了---
其实也不会有太大影响。我可能不会在每次改动几行代码后都运行测试,而是只运行一些特定的测试……但对我来说影响应该不大。
Mat Ryer: 是啊,真的很神奇。当我按下保存键时,我会编译并运行测试,如果有测试失败,我会在 IDE 里显示出来。Go 的快速编译时间从一开始就存在,虽然有些波动,但这是另一件我们可能视为理所当然的事... 如果它消失了,我们肯定会想念它。
Johnny Boursiquot: 是的,正如 Ian 所说,如果编译时间有明显的影响,那对于语言、编译器和开发者的工作流程来说都是一个巨大的打击。我不想因为使用泛型而让我的工作流程受到影响。我想这不是任何人希望的。
Mat Ryer: 是的,听起来他们也非常关注这个问题。我同意,这确实很重要。还有一件事,我经常听到有人避免使用 defer
,因为 defer
会有一点性能损耗。然后我发现他们的使用场景根本不会受到任何影响。人们有时会对“如何从某些东西中榨取每一点性能”有些执着。实际上,可读性---
你作为开发者修复代码时的效率如何?那种性能呢?
Ian Lance Taylor: 是的,完全正确。既然你提到了 defer
,我就插一句,在 1.14 版本中,我认为 defer
的性能将大大提升。目前有一些这方面的工作正在进行中。
Mat Ryer: 我之前没为 defer
付费。每次使用 defer
时,我应该付费吗?不过我几乎愿意,因为它真的太好用了... [笑声] 这是我最喜欢的 Go 关键字之一。是啊,这真的很令人兴奋。我喜欢的一点是,当 Go 团队在努力改进标准库、改进编译器工具时,我们不用做任何事情就可以享受这些好处... 所以我真的很感谢你们为我们辛苦工作,感谢你们。
Ian Lance Taylor: 不用谢,不过你知道---
很多改进并不是来自 Go 团队。很多改进来自社区中的其他人。我们做了很多协调工作,但很多实际工作是由外部完成的。所以也要感谢所有人。
Mat Ryer: 这听起来很棒。
Johnny Boursiquot: 这真的很酷。还有一点,我认为 Go 2 的版本命名似乎并没有特别的推动力。我认为即将引入的变化,像合同(contracts)和泛型,是向后兼容的,基本上仍然保持了 Go 1 的承诺---
你的代码仍然可以正常工作... 我觉得这真的很了不起。
Ian Lance Taylor: 是的,这也是我们一个重要的目标。
Mat Ryer: 这意味着这些改动可以进入即将发布的 Go 版本,而不必等到 Go 2 吗?
Ian Lance Taylor: 是的,Go 2 目前更多的是一个概念。这么说吧,我们会尽量保持 Go 1 的兼容性。如果必须做出某些破坏性改动,那我们可能会做,但我们会尽量避免。也许在某个时候,或许在泛型落地之后,或许在更多错误处理改进到位之后,或许等模块(modules)稳定后,我们会称之为 Go 2。这样做可能是个不错的营销策略,可能听起来比较好;也可能给大家一个重新了解这门语言的理由... 但这并不意味着 Go 1 的程序会停止运行。
我喜欢用的例子是,你写一个 C 程序---
不一定是 1970 年的 C 程序,但大概 1980 年的 C 程序今天仍然可以运行;C 语言从未有过一个 C 2,所以为什么不模仿这种模式?它是一种非常成功的语言。
Mat Ryer: 是的,完全同意。事实上,我喜欢 Go 2 甚至可以移除一些东西的想法。但当然,这意味着会有破坏性改动。不过我喜欢看到这样的破坏性改动---
当我们让语言变得更简单时。
Ian Lance Taylor: 同意。
Johnny Boursiquot: 比如移除 panic
?[笑]
Mat Ryer: 哈哈... 移除全局状态。
Johnny Boursiquot: 这可是挑衅啊。
Ian Lance Taylor: [笑]
Jon Calhoun: 你们越来越贪心了。
Mat Ryer: 是啊。你知道它们引入了随机映射(random to the map),因为人们滥用了它?顺便说一句,我现在就滥用它来获取随机的东西,但我是用另一种方式滥用的。是的... 会是一样的。
Jon Calhoun: 我有一个问题---
我们谈了一些关于语言会如何改变的事情,也谈到了当人们接触 Go 后,他们会想要大量使用 Channel,尽管这并不是正确的工具... 我确实有一个担忧,很多人也会有同样的担忧---
一旦我们有了泛型,人们就会想要使用它们,所以他们会写出这些泛型实现的数据结构库,或者其他东西... 我认为 Go 社区现在有一个很好的习惯,就是不会随便引入依赖,他们会自己写东西。但如果我们有了泛型,你觉得这会改变这种心态吗?
Ian Lance Taylor: 嗯,我不知道。我觉得这是个好问题。我也不确定。你们觉得呢?
Mat Ryer: 我觉得我们已经有了一个空间,可以讨论包的质量。我看过几次很好的演讲---
比如 Julie Qiu 做过一个关于如何有意识地选择依赖的演讲,而不是随便从任何地方拿来一个依赖;她提到应该看看项目是否健壮,是否被广泛使用,是否有测试,API 看起来如何,文档怎么样... 考虑这些因素。这会变得更重要,因为一旦泛型进入语言,大家可能会很容易地去使用这些库,我们会看到大量新的库涌现,做各种很棒的事情,然后我们就会面临选择的难题... 我认为这是一个我们社区仍然面临的普遍问题---
不知道哪些依赖可以信任,哪些依赖只是一些不太适合用于生产代码的实验性项目。
Johnny Boursiquot: 我认为这是社区自然会解决的问题。我们之前提到的实践,比如滥用 Channel,或者不论程序是否需要都使用并发... 这些问题我们已经解决得差不多了,社区中也有足够多的材料来教育大家---
“尝试这样做,避免那样做,这是基于 X、Y 和 Z 原因。”经过这些年的发展,我们形成了所谓的 Go 习惯用法,基本上是采用某些方法。
我认为没错,刚开始时你会看到大量使用合同(contracts)和泛型功能的东西,但我认为当我们自己踩了足够多的坑之后,情况会逐渐平稳下来,最终形成所有 gopher 都理解的 Go 习惯用法。
但我确实有点担心新手,那些来自其他语言的开发者,或者第一次学习编程的人,他们刚好使用 Go 学习---
如何教他们这些概念。因为这需要你在多个层次上思考不同的事情,才能真正理解这些特性在哪里有用。
我认为 Go 博客上那篇 Why Generics[7] 文章做得相当不错,介绍了“这是你现在会怎么做。”你需要为字符串写一个反转函数,为整数写一个反转函数,而泛型可以帮助你去除这些样板代码。
这些材料对于正确教授如何使用这些语言特性至关重要,我认为这也是一个自然发生的过程。
Jon Calhoun: 对我来说,我想到的一个例子是围绕 Go 的路由和 Web 框架... 我觉得如果有类似 Gorilla 工具包的东西,它提供了所有这些不同的 Web 工具,可以使用它们。我想现在可以说它们都经过了充分的战斗测试,是很好的工具。而有一个类似的泛型和数据结构工具包会很有用...
但我也担心你可能会遇到这种情况---
我们有 20 种不同的实现,20 个不同的路由器,它们都在相互进行基准测试,关注错误的细节。所以一方面,我确实希望社区能够搞清楚并达成某种共识... 但我也看到其他方面的情况,我并不 100% 确信这会发生。
Mat Ryer: 是的... 但这实际上不是泛型的问题,我想。这是社区的问题。
Jon Calhoun: 这是社区的问题。我只是希望泛型不会让这种情况变得更糟。你会想“路由器有多少不同的东西可以改变呢?”其实真的不多。但在数据结构方面,你可以改变的东西就很多了。
Mat Ryer: 是的,没错。我想我们只能拭目以待。
Mat Ryer: 嗯,没错。我觉得我们还需要再观察一下。我喜欢给出的一个建议---
这几乎是我们可以用来检验任何改变建议的非官方测试……我会告诉大家---
有人提到他们对数组和切片的困惑,我会说:“现在只需要学习切片,这样你就可以先提高生产力,之后你可以根据需要了解它的底层工作原理。” 这有点像“及时学习”;这是学习的最佳时机,因为你有需要了解它的上下文。
所以我会告诉大家:“不用担心它。” 如果你可以说“别担心它……” 泛型---
最新的提案绝对通过了这个测试,“不用担心它。”
像你说的那样,在阅读文档时有几种情况你会看到这些泛型函数---
它们看起来有点不一样,所以你需要知道如何调用它们……但尤其是在类型被推断的情况下,你几乎可以忽略它是泛型的事实。这是它的一个优点,所以我认为这肯定会有所帮助。
Ian Lance Taylor: 我当然希望如此。
Jon Calhoun: 那你觉得我们作为社区能否开发一些工具,以便更有可能实现这一点?我在想的是---
Mat,你提到过你不想过早进行优化,不想在写一次具体实现之前就把它做成泛型版本……所以如果我们有一些工具能让你轻松地将写好的实现,比如一个自平衡树,转换为泛型版本,这会不会帮助社区避免过早优化?
Ian Lance Taylor: 这是个有趣的想法。我觉得这样的工具应该挺容易写的。至于人们是否会觉得它有用---
我不确定。或许吧。
Mat Ryer: Jon,或许你可以贡献一下这个工具。
Jon Calhoun: 你看,Ian 说这个工具很容易写。我怀疑他写起来会比我容易得多。
Ian Lance Taylor: [笑] 不一定。
Mat Ryer: 嗯,我不知道。但我可能会赌他写起来确实容易些。不过我还在想关于处理器和 HTTP 方面的事情,可能会发生什么变化。还有,关于上下文---
我们是否会看到带有泛型风格的方法,比如取值……我不知道这是否可行。能否在一个非泛型的类型中仅有一个泛型方法,或者……?
Ian Lance Taylor: 在当前的设计草案中,不,它是不允许的。原因是它使得理解带有泛型方法的类型何时实现接口,或者可能是泛型接口变得更加混乱。我们在这上面遇到很多令人困惑的问题,因此决定---
没必要,因为你总是可以写一个泛型函数来代替,所以我们就把方法排除掉了。如果我们以后能更好地理解它,或许可以将其添加到语言中,但我认为它不会出现在第一个版本中。
Jon Calhoun: 我有一个问题。在其中一个例子中,你提到一个契约,比如说是数值类型,或者类似的东西,基本上涵盖了所有不同的整数类型和数字……我猜有时候查看零值是有用的。你觉得---
我想问的是,你如何与这些常量值做比较?这会改变编译器处理它的方式吗?还是有一些特别的东西?
Ian Lance Taylor: 不……我觉得,这又回到了你可以使用的不同编译策略。有几种不同的方式可以让编译器处理它。从语言的角度来看,这非常简单。如果契约允许的所有类型都可以与零进行比较,那么你就可以写一个与零比较的代码。至于它究竟如何编译---
可能有一些有限的类型,然后你为每种类型编译;或者你使用类似方法的方式,实际上传递“这是你如何与零比较的代码”。我不确定哪种方式在编译时是最佳的。
Mat Ryer: 好的,我觉得今天的时间差不多了。非常感谢我们今天的特别嘉宾 Ian Lance Taylor,他正在致力于泛型提案。Ian,我觉得你做得很好;我个人非常喜欢最新的提案。如果你还没有看过,可以去网上查找一下。而且 Ian 的演讲现在也可以看到了;如果你搜索 “GopherCon Generics 2019”,你会找到 Ian 的演讲。还有 Johnny 的演讲以及我的,但如果我宣传自己的演讲就太不合适了…… [笑声]
Jon Calhoun: Mat 从来不会做这样的事情。
Mat Ryer: 这不是我的风格,这不是我的风格。不过我的书还在卖。[笑声] 好的……再次感谢我的其他嘉宾,Jon Calhoun,Johnny Boursiquot……先生们,非常感谢。下次见,拜拜!
Ian Lance Taylor: https://www.airs.com/
[2]#98 Generics in Go: https://changelog.com/gotime/98
[3]Mat Ryer: https://github.com/matryer
[4]Jon Calhoun: https://github.com/joncalhoun
[5]Johnny Boursiquot: https://github.com/jboursiquot
[6]这个视频: https://www.youtube.com/watch?v=WzgLqE-3IhY
[7]Why Generics: https://blog.golang.org/why-generics