本篇内容是根据2020年11月份Go Time-What would you remove from Go?音频录制内容的整理与翻译, 几位Gopher讨论了希望Go中哪些特性能够移除(当然只是讨论,并不可能真实发生),包括.import
隐式导入,goto
关键字等,以闲聊为主,信息密度并不高。
过程中为符合中文惯用表达有适当删改, 版权归原作者所有.
Mat Ryer:
大家好,欢迎收听 Go Time!我是 Mat Ryer。今天我们要讨论的是:如果你可以从 Go 语言中移除某些东西,你会移除什么?嗯……你可能会觉得这是一个很奇怪的话题。和我一起揭开这个话题的是 Jon Calhoun。你好,Jon。
Jon Calhoun:
嘿,Mat。
Mat Ryer:
最近怎么样?
Jon Calhoun:
还不错。
Mat Ryer:
很高兴听到这个。
Jon Calhoun:
我们还有 Johnny Boursiquot。Johnny,你怎么样?也不错吧?
Johnny Boursiquot:
是啊,我喝着牛奶,来到了这个节目,据说今天的节目全是些不太受欢迎的观点……所以我已经准备好了!
Mat Ryer:
好吧,看来今天会很有意思。我们还有 Daniel Martí 也加入了。你好,Daniel。
Daniel Martí:
嘿,很高兴再次回来。
Mat Ryer:
你很受欢迎。谢谢你加入我们。我想我们应该先讨论一下为什么这个话题会如此有趣……作为开发者,我们通常非常关注新东西和新特性。每当 Go 添加了新特性,像是泛型或错误处理的变化时,我们都会很兴奋。但从 Go 中移除东西,有什么价值?为什么这是一个值得去做的事情呢?
Daniel Martí:
我认为一个小型语言有两个好处:一方面它更容易学习,另一方面它也更容易阅读和维护。很多年前,我还没接触 Go 时,我用的是 C++ 或 Python。几年后,我写 C++ 或 Python 的方式完全不同了,因为这些语言变化得太快了……而且我甚至无法读懂自己写的代码。但在 Go 中,这种情况几乎不会发生。你今天写的 Go 代码和五年前写的基本是一样的,或者说非常相似。
Mat Ryer:
对。
Jon Calhoun:
我觉得另一个好处是能减少团队间的摩擦。不用再因为如何定义变量而争论不休,大家可以更快地投入工作,这很有用。
Mat Ryer:
对,Go 的 gofmt
工具会自动格式化所有代码,这样就不会再有人讨论代码应该如何格式化了,对吧?
Jon Calhoun:
是的。我觉得用过功能更多的语言的人都会明白,当你有七种方式去做同一件事时,就会有七个人认为不同的方式最好。
Mat Ryer:
嗯,这确实很有意思。一种更小的语言往往只有一种方式去做事,这对代码的可读性和可维护性是很有利的……还有学习难度。如果你想知道如何做某件事,只有一种方式时你会更容易找到答案。
你应该看看在 JavaScript 中有多少种方法可以将数组相加……有一些方法确实很神奇,同时也很吓人。但在 Go 中你不会遇到这种情况。我觉得有时候外界的人初看 Go 时,会觉得它的简单是一个缺点……但实际上,这恰恰是它最大的优势之一,对吧?
Jon Calhoun:
我觉得这也是一种相乘效应。比如当我还在用 Java 时,尤其是在 Android 开发中,你会查看 Android API,想知道 “这个 API 到底是干什么的?” 如果文档写得不够好,你就只能去看代码。但你会发现有一个类继承了另一个类,那个类又继承了一个抽象类,最后你得穿过五层抽象……对我来说,这让事情变得非常难懂。而在 Go 中,你点开 godoc 几乎总是可以直接看到代码。
Mat Ryer:
对,这很真实。类的层次结构在特定场景下非常强大……但我也曾犯过这个错误。我曾陷入这种模式,创建了非常复杂的类型结构、抽象类和泛型,尤其是在 C# 中。你甚至可以为泛型设置约束条件,不仅是任何类型,而是必须具有某些属性的类型。这感觉非常棒,尤其是当你搞清楚这些并使代码正常工作时。但当你稍后再回来看这些代码——不需要过很久,我的代码就像陌生人写的一样,我根本搞不清楚。我也因此学到了教训,现在会很谨慎地使用这些功能。
Daniel Martí:
是的,我觉得这和 Jon 之前提到的一点有关,它对团队合作非常有好处,因为减少了许多摩擦……但对我个人来说,它也减少了我与过去的自己和未来的自己之间的摩擦。因为我两年前写的代码,我可能已经不记得大部分细节了。所以语言的简洁性迫使我——我不能说它让我 “保持简单”,但至少它限制了我能做的 “魔法”。
Johnny Boursiquot:
“简单”是一个非常主观的概念……对我来说简单的东西,对别人或许并不简单。甚至 Go 社区中常用的 “可读性” 这个词也是非常主观的。我很好奇是否有研究或数据能够证明代码的可读性。我自己曾经看到过一些研究,不是专门针对 Go 的,但我真的很想看看是否有数据能够支持我们对 Go 的这些定性评价。
Mat Ryer:
用某种方式衡量代码的可读性……
Johnny Boursiquot:
对。
Mat Ryer:
这确实很有趣。正如你所说,这在某些方面是主观的。当然,如果你去衡量一个开发者修复某个代码库中的问题需要多长时间,然后对比结果,我能想象得到一些有趣的结果。但这很大程度上取决于开发者本人。不过就 API 设计而言,我们可能可以说 “少即是多/更简单”。但也许不尽然,因为在某些情况下,添加一个类型可能会很好地解释某些东西,即使你不一定需要那个类型。所以这确实是一个有趣的话题。
Johnny Boursiquot:
是的,在某种程度上,一些语言拥抱了用非常表达性的方式在文件或项目中传达意图的概念。通过不同的上下文使用不同的关键字,可以使代码在某个上下文中更具可读性;而在另一个上下文中,做同样的事情但使用不同的关键字,可能会使代码在那个上下文中更有意义。
比如 Ruby,我熟悉的语言之一,就有这种表达性的特点。同样的事情可以用不同的方式表达,而 Ruby 的价值之一就是语言的表达力。
而在 Go 中,我从未听到我们讨论 Go 语言的表达力。我们通常提到的是——忘记这个双关语——简洁。它简单易读,关键字少……基本上就是 “少即是多” 的理念。但少真的就是多吗?如果我扮演一下反方角色,是否更多的表达方式——比如 Ruby 的表达力——是否可以让我们写出比当前 Go 语言更具可读性的代码呢?
Mat Ryer:
是的,这是个很好的观点。这几乎是另一个话题,关于我们可以如何改进和扩展 Go……但确实,当你开始思考这些时,真的很有趣。不知道有没有其他人想补充的。
Daniel Martí:
最近有个例子可能与 Johnny 说的方向有关,人们希望有一种惯用的方法来快速删除 map 中的所有元素。有些人建议添加一个内置的 DeleteAll 方法。但最终,他们教会了编译器识别一个简单的循环模式,逐个删除元素,并将其转换为高效的 DeleteAll 操作。
这是一个权衡问题。如果你添加一个新的方法来删除所有元素,那么突然之间人们就有了两种选择,语言也因此变得更复杂。所以这是一个高层次操作和低层次操作之间的取舍问题。
Jon Calhoun:
这些取舍也与上下文密切相关。你工作的公司规模、目标等都会影响这些决定。如果你要做一个可读性研究,我几乎可以想象,小团队使用表达性更强的语言可能会更有效率地处理新代码,尤其是在团队内部的新代码。
而我认为 Go 的可读性之一在于你可以快速进入代码,不必是你公司写的代码,几乎任何 Go 社区的代码你都可以读懂并帮助改进。但在一些更具表达力的语言中,由于每个人都有自己的意见,可能在小团队内效率很高,但一旦你离开那个小团队,与有不同意见的人合作时,事情可能会变得更加缓慢。
Mat Ryer:
下次 Johnny 提出要求时,不要让他要求证据、科学数据什么的……这让我们要做的工作变得太多了(笑)。
Jon Calhoun:
告诉他多喝点牛奶吧。
Johnny Boursiquot:
是的……
Mat Ryer:
不过,总的来说,Johnny 提出的观点还是非常好的。那么,回到正题,是否有一些语言或标准库中的例子,如果移除它们,我们会觉得更好?比如我们刚才提到的只保留一种方式做事的思路,或者在表达想法时的优化,是否会对可读性产生积极或消极的影响?我们可以深入讨论一下。如果我们有不同意见,大家可以用嘴巴发出蜂鸣声,编辑会后期加上合适的音效。
Johnny Boursiquot:
(笑)
Mat Ryer:
我保证……如果你有不同意见,就可以捡起抛下的手套。因为我们可能在某些问题上并不一致,我觉得这其中也包含了很多个人的喜好。那么,谁愿意先来举个例子?Daniel,你会从 Go 中移除什么?
Daniel Martí:
我想从语言特性开始……我觉得 .import
应该完全移除。.import
是一种导入语句,它以一个点开始,表示该包中的所有导出名称都会直接出现在当前包的作用域中。我不需要像 foo.某某
这样使用包名前缀。
Johnny Boursiquot:
所有喜欢 DSL 的人现在都在用异样的眼光看着你……
Daniel Martí:
我觉得在某些特定情况下,DSL 的使用场景是合理的,但这种情况非常罕见,我不认为 Go 需要为此保留一个特性。几乎每次我看到 .import
,我都会想:“真的有必要这样吗?” 比如在测试中,它让测试代码变得非常难读。你看到一个函数调用时会想,“这个函数是哪里来的?”然后才发现原来有个 .import
。
Jon Calhoun:
这个问题很有趣,因为我几乎没想到这一点,因为我已经好几个月没见过 .import
了……所以根本没想到。
Mat Ryer:
是的,我也没怎么见过。
Jon Calhoun:
所以我不反对你说找不到好用例,但它没在我的优先级列表上,因为它似乎没有被滥用。所以我觉得无所谓……
Mat Ryer:
这可真是太残酷了(笑)。Daniel 好不容易来告诉我们他想移除什么,而你却说“这对我来说不是优先考虑的”。
Jon Calhoun:
我只是说这不是我的优先事项。如果它是他的,那也没关系。
Mat Ryer:
你根本不见它也不用它。
Jon Calhoun:
如果把它移除了,我也不会知道。所以这在某种程度上是支持他的论点……我根本不会知道它被移除了。
Johnny Boursiquot:
等一下,我们犯了一个严重的错误。我们假设每个听众都知道我们在说什么……所以我们先退一步。Daniel,你提到了 .import
,能不能解释一下它的作用,以及它能实现什么?
Daniel Martí:
当然。如果你导入一个名为 foo
的包,想要使用它的任何东西,你需要写 foo.bar
,例如调用一个导出的函数 bar
。而如果你用 .import
导入这个包,也就是在导入语句中以点开头,然后是包名 foo
,那么你可以直接使用 bar
,不用加 foo.
前缀。
简而言之,它让你可以直接使用那些名称,就好像它们是在当前包中定义的一样。
Mat Ryer:
但当然,这会在可读性上有所损失,因为你乍一看不会知道这是否是一个本地方法或者当前包中的东西。所以让所有类型使用包名前缀确实在可读性上有很大的提升……我同意你的看法。
还有什么好处?难道只是为了减少按键次数吗?
Daniel Martí:
嗯,作为优点,我不知道,因为我是在主张移除它……
Mat Ryer:
但这对我们来说不是优先事项(笑)。
Jon Calhoun:
但我觉得命名是裸返回的要求,对吧?
Mat Ryer:
对。
Daniel Martí:
是的,但你可以命名结果,而不使用裸返回。这就是我想说的。
Jon Calhoun:
对。
Mat Ryer:
对,对,对。
Jon Calhoun:
我经常使用命名返回值,这样可以更清楚地说明每个变量是什么……但如果你看我的代码,你可能根本不会意识到我在使用命名返回值,因为我并不经常以那种方式使用这些变量。老实说,有时我甚至希望它不要自动为我初始化变量,因为有时我会去初始化变量,却没有意识到“哦,它已经初始化了,因为这是一个命名返回值”。不过,虽然我确实看到了命名返回变量的用途,但我真的不喜欢空返回。
Mat Ryer:
有人想为保留裸返回或命名返回参数辩护吗?
Jon Calhoun:
比如说,当你从 panic 中恢复时,我知道你会使用命名返回值……但一旦你进入 defer 块,你必须使用裸返回吗,还是可以……我不太确定那会是什么样子。我知道你可以直接 return nil, err
,这应该是可以的,但我觉得你仍然需要分配错误。我不太确定。
Daniel Martí:
那是因为 defer 函数不返回任何东西……
Jon Calhoun:
哦,对。
Daniel Martí:
……所以你需要命名返回参数的唯一原因是为了在父函数中可以重新分配它们。
Jon Calhoun:
但是你不必在 defer 块中使用裸返回,对吗?
Daniel Martí:
没错。我之前提到的可能是搞混了这两种特性。
Jon Calhoun:
好的。我想说,这就是难点……我觉得裸返回之所以出现,可能是因为命名变量已经在那里了……不过我确实同意,去掉裸返回会让代码更清晰……
Mat Ryer:
对。
Daniel Martí:
我觉得如果去掉裸返回,大家可能会希望有一个特性:比如,你想返回一个结构体的零值,你必须在组合字面量中写出结构体的名字。你必须写一个很长的类型名,加上花括号,因为这是零值。如果你可以用类似 _
这样的东西来表示“这个东西的零值,我不在乎”,那就好了。因为裸返回确实给了你这种简洁性,如果没有它,你就会失去这种简洁。
Jon Calhoun:
那么,写一个 lint 工具来找到所有的裸返回并自动加上变量,不是也可以吗?因为你必须有命名返回变量……
Daniel Martí:
当然可以。
Jon Calhoun:
我觉得这应该是 Bill 的下一个项目。
Daniel Martí:
谁想死在那个地狱里?
Mat Ryer:
什么时候会用到它?如果你需要它,手动输入不就好了。
Jon Calhoun:
你可以在你的工具链中配置一下,这样如果你有空返回,它就会自动替换它们。如果 Bill 让所有人都使用它,他再也不会看到裸返回了。
Mat Ryer:
或者让 Bill 参与每个人的 PR 流程,然后他自己来改。
Jon Calhoun:
Bill 已经很忙了,我不确定他能不能搞定。
Johnny Boursiquot:
有人会写一个叫 Bill 的 linter……然后它会帮你修复返回值。
Daniel Martí:
也许它会给你一个徽章,如果一切都通过了。
Mat Ryer:
一个徽章……哦,对,像奖章一样。
Johnny Boursiquot:
对。可以别在你的帽子上之类的。
Daniel Martí:
我觉得 Johnny 刚才还想说一个他会去掉的特性……
Mat Ryer:
Johnny,你有吗?你会从语言或标准库中移除的东西?
Johnny Boursiquot:
我有一个,但我的反对立场已经软化了……
Mat Ryer:
从什么时候?
Johnny Boursiquot:
我一直在找理由去使用它,去喜欢它,我一直在努力……
Mat Ryer:
你终于找到了。
Johnny Boursiquot:
对……我可能仍然不会用它,但我能理解那些使用它的人;我试图理解他们的想法……Go 中的标签和 goto
的使用。
Mat Ryer:
对。
Johnny Boursiquot:
我很少会觉得自己需要它。如果我感觉需要用到它,我会重写代码,不使用它。
Mat Ryer:
对。给那些不熟悉的人解释一下,Go 确实有 goto
……如果你回去听 Johnny 刚才的双关语,它其实是双重的……因为它包含了 “go” 和 “goto”。它们被认为是“意大利面条代码”的罪魁祸首……因为在 BASIC 语言中,代码就是这么写的。你会有行号,比如 10、20、30,然后这些代码就在这些行上……它们是按 10 递增的,这样你可以在中间插入其他指令……
Johnny Boursiquot:
后来……
Mat Ryer:
对。因为你已经写好了行号,所以已经太晚了……我不知道什么时候他们发明了动态行号,但那确实改变了世界,让我告诉你……然后他们会用 goto
来跳转流程。在一些语言中,它们没有函数、子程序之类的东西,而这些东西基本上就是做那个的;它们为你做了跳转,但以一种安全的方式,你声明了输入输出。
所以 goto
因为让代码难以理解而出名……它有点像那些自己选择冒险的书,你会跳转到某些特定页面。很难理解这种跳转。破解这些游戏也很难;你必须真的去玩它们。
不过标签还有另一种用法……就是当你想跳出循环时。如果你有几个嵌套的循环——就算只有两个——在某种深层情况中,可能你想退出当前的这个循环,这很容易用 break
实现,但你可能还想退出外层循环,你可以设置一个标志,然后检查标志,再 break
。
但标签允许你退出特定的循环,这有点奇怪……不过正如你说的,Johnny,我也见过一些合理的例子……通常在代码非常小、简洁的时候;而不是在那些大而长的多页函数中。
Jon Calhoun:
为了确认一下我的记忆……标签可以用在 goto
、break
和 continue
吗?还有其他的吗?
Daniel Martí:
我想就这些。
Johnny Boursiquot:
没了,就这些。
Jon Calhoun:
我不确定 fallthrough
或其他关键字是否也能用标签。
Johnny Boursiquot:
不
Mat Ryer:
另外,当你说 continue
时,你可以指定“继续这个特定的循环”。
Johnny Boursiquot:
对,继续到一个标签。
Jon Calhoun:
是的,你可以继续到一个标签。所以如果你在嵌套循环中,你可以说“继续”,然后它会跳到另一个循环继续执行。
Mat Ryer:
你实际上是在给 for
块打标签……
Jon Calhoun:
对。
Mat Ryer:
……然后你说“继续这个”。
Jon Calhoun:
对。所以你跳到你想去的外层循环。
Mat Ryer:
对。这是魔法,是不是?是黑魔法。
Jon Calhoun:
我同意 Johnny 的观点,我见过一些人对它们提出了还不错的论点……但我自己从来没有想用过它们。总觉得有更简单、更干净的方式,比如使用嵌套函数或其他什么东西……对我来说,总有其他东西比使用标签更好。也许这只是个人偏好,我不能确定,但……既然它们存在,我不确定创建这个语言的人看到了什么我没看到的理由。所以我很难说“别用它们”或“把它们去掉”。
Mat Ryer:
对,我觉得听众们应该对这期节目稍微持保留态度。我们只是讨论我们想去掉的东西。请随意使用这些特性。它们是语言的一部分……但显然,如果你有点常识,听听我们说的,因为我们犯过所有的错误。[笑声]
Jon Calhoun:
我觉得更好的说法是,如果我在审查代码时看到标签,我可能会建议修改。
Mat Ryer:
你知道吗——我确实用过它,但只是在非常具体的情况下,这是最清晰的做法,那就是你明确表示在这种情况下我们要停止并打破整个流程……但你在另一个逻辑流程中。不过,是的,我的意思是——你总是可以重新设计代码来避免这些问题。
Daniel Martí:
我要再次使用我的蜂鸣器了,BZZ!! [笑声]
Mat Ryer:
能不能多录几次蜂鸣声,给编辑用的……?
Daniel Martí:
不了,我没问题,谢谢……[笑声]。我想说的是——我觉得两者都有点道理。我同意,我不经常使用标签,可能一个包里最多用一次或两次,但当我使用它时,如果被迫拆分一个函数——比如一个有两层缩进的 60 行函数——我觉得被迫拆开并不是好事……
而且我还想为 goto
辩护。或者说,我有两个例子。一个是类似重试的习惯用法;能够用 goto
跳到重试位置,重新执行一个函数,这非常有用。你可以用 for
循环来实现,但如果你从头开始,for
循环看起来像是一个无限循环……只有在结尾你才会发现“哦,等等,我要不要跳出循环?”,老实说,我觉得这并不更好……
另一个用例是代码生成。比如,如果你想生成一个自动机,或者某种在状态间跳跃的机器人,使用 goto
非常有用。
Jon Calhoun:
这很难,因为你几乎需要看到具体案例,才能真正判断是使用标签更好,还是用其他方法更好……当你说“拆分成一个函数”时,我会想,能不能写一个匿名函数,或者一个闭包,直接放在那里,这样会不会更好——我不知道,这可能取决于具体情况。
我反对标签的另一个论点是它们太少见了,所以我觉得一个刚接触这门语言的人会想“这是什么?”虽然其他方法可能不那么直观,但它们至少是初学者熟悉的东西,可能更容易理解。
Mat Ryer:
对。这种情况下你只能在 PR 评论中争论一番了,我想……
Johnny Boursiquot:[笑声]
你看,这就是问题所在——对我来说易读的东西不一定对你来说易读……所以对于某些人来说,看到 goto
一个标签,可能觉得完全合理。他们会说“哦,是的,我完全理解这里发生了什么。你到达这里,然后需要跳出这些循环,你用了 goto
,没问题,这很合理。”
而像我这种不经常使用它的人——仅仅因为我不经常使用它,并不意味着它不好,对吧?这只是意味着我通常不使用它。当我第一次看到它时,我可能会挠头想“为什么你要这么做?”然后我会像 Jon 那样在 PR 中争论一番,逼你把它去掉。[笑声]
Jon Calhoun:
我可以举一个例子,比如你有三个嵌套的 for
循环,最里面的一个会 continue
到一个在第一个 for
循环开始位置下方的标签,所以它实际上是在第二个循环中,我觉得在那种情况下,只有第二个 for
循环会被继续……但老实说我不知道。所以我会想“我得运行一下这段代码,才能弄清楚它到底在做什么”,这会让我有点沮丧。
Johnny Boursiquot:
你是说你不会为每个 PR 运行代码吗?
Jon Calhoun:
我不会坐在那里运行这些 for
循环……虽然我也不经常遇到三重嵌套的 for
循环,所以……
Johnny Boursiquot:[笑声]
Mat Ryer:
那就在脑子里运行它们吧。你可以在脑子里运行它们,你知道的……
Jon Calhoun:
我是说,这不就是 CI 的作用吗?
Mat Ryer:
对。
Daniel Martí:其实,我有点同意。我觉得 continue
和 break
,我只会用它们来跳出一层父结构,不会多于一层……因为当你跳过多于一层时,事情就开始变得混乱。所以,也许你可以用标签来替换 break
,但不是这个父结构……而是仅仅跳出直接的父结构。
Mat Ryer:嗯,好吧,我们就这么做。说得好。顺便说一下,谢谢你,丹尼尔;当你为 goto
辩护时,你其实做了三个双关语。你提到了 Go 语言,goto
本身,还有因为你给了两个理由……“二”也是一个双关。
Daniel Martí:这不是故意的……
Johnny Boursiquot:马特总是记得这些。[笑声]
Mat Ryer:是的,我把它记在我的双关笑话本里。
Johnny Boursiquot:[笑]
Daniel Martí:让我看看我的 goto
双关笑话……
Mat Ryer:你看到了吗?我在 GothamGo 做过一个演讲,叫做“我在 Go 中不会使用的东西”,实际上就是关于这个话题,讨论了同样的问题……我还提到了 else
语句……这个话题常常引起有趣的反应,因为听起来好像——当然你需要 else
;你在说“如果这样”,然后如果不这样,你就需要做点别的事情。但实际上,我的重点是代码的可读性问题,先处理错误情况,处理边缘情况,像乔恩之前提到的那样,把理想的路径放在左侧。
所以 else
是个有趣的东西……如果你发现自己有一个很大的 else
块,或者很多 if-else
块,有一个技巧是你可以翻转 if
语句的逻辑。如果你写的是“如果做某事”,你可以把它翻转成“如果不做某事”,然后处理 else
的情况,这样你就可以回到主路径。其实本质上是一样的,只是一种写作风格。
你们经常用 else
吗?约翰尼,在过去的七天里,你用了多少次 else
?
Johnny Boursiquot:零。我可能——嗯,我之前用过……
Johnny Boursiquot:我本来想说,我几乎数不清我在使用 Go 语言的整个过程中用了多少次 else
,但这可能有点夸张。是的,else
并不是我经常用的东西……每当我需要用到 else
时,我都会认真思考“有没有办法我可以提前返回?”或者像你说的那样,重新考虑我在做的事情,看看是否能避免使用 else
。一旦我看到 else
,我就会开始挠头,觉得“嗯,这看起来不太对劲。”
当然,else
是语言的一部分,这并不意味着你应该避免使用它,有时候确实需要用它……但通常情况下,即使在编写函数时我知道会有条件语句,我已经提前在想,如何避免使用 else
。这已经成了我的第二天性。
Mat Ryer:这很有趣……是为了可读性和代码的流畅性。
Johnny Boursiquot:是的。
Mat Ryer:这是一个相对简单的技巧,如果你刚开始写 Go,记住这一点会有所帮助。有些情况下,最清晰的逻辑就是一个五六行的 if
语句,“如果这样,就设置点别的”。有时候这种逻辑正是你需要的。但如果你不注意保护代码的结构,当你在一个函数中有两三个这样的 if
语句时,你真的会嵌套得很深,浪费了很多缩进。你不需要那么多缩进……那是种浪费。
Jon Calhoun:马特说缩进太多了。
Johnny Boursiquot:但如果你把它们换成空格呢?
Jon Calhoun:那你应该用 go fmt
。
Mat Ryer:嗯,空格占的更多,不是吗?
Johnny Boursiquot:标签与空格之战,开始!
Mat Ryer:嗯,在 Go 里面标签赢了,因为 go fmt
用的是标签……
Johnny Boursiquot:[笑]
Mat Ryer:但是……所有这些空白字符都要传送到 GitHub 上……那是种浪费。你知道 GitHub 上有多少空白字符吗?没有人想过要去统计一下。他们应该去数一下。那可是一堆……
Jon Calhoun:这是你的下一个项目?
Mat Ryer:……而且全是空的。真是浪费。
Jon Calhoun:他们需要一个网站,每天更新……
Mat Ryer:对,他们已经有了。他们有那些每天更新的网站。它们叫做新闻网站。[笑]
Jon Calhoun:统计空白字符数量。
Mat Ryer:对,那是最难的部分。
Johnny Boursiquot:你知道有一种语言浪费了大量空白字符吗?我记得它叫 Python。浪费了好多空白字符……[笑]
Jon Calhoun:哦,天哪……
Mat Ryer:嗯,对我们来说那只是空白字符,但如果你在白色页面上看它,那里仍然有字符,仍然是数据。这是一场灾难;我们需要减少这些空白字符。气候变化可以稍后再解决……我们应该先解决这个问题。
Jon Calhoun:我感觉比尔就在我们的窗外抗议……[笑声] 他已经在 Slack 中掌握了这一点……
Mat Ryer:他在外面喊着,寻找“裸返回”……[笑声]
Jon Calhoun:但现在他希望我们讨论移除接口返回的能力。
Johnny Boursiquot:哦,天哪……
Jon Calhoun:……除了空接口。
Daniel Martí:我不同意。我明白他的意图;如果你有一个构造函数,它应该返回一个特定的类型,而不是接口,而且在大多数情况下,你不希望返回非空接口……但在某些情况下,这是可以的,只要你知道自己在做什么,我觉得没问题。
Jon Calhoun:我喜欢偶尔返回接口的部分原因是,我觉得它更清楚地表达了你的意图。如果我有一个函数,它设置了一个小服务器,我只想返回一个处理器……我有时希望有能力改变我的实现方式,只返回一个处理器。有时候,在我看来,直接说“你只需要关心我给你的是一个 HTTP 处理器就行了”会更简单。具体的实现细节此时不应该打扰你。但我确实认为这种情况比较少见……
Mat Ryer:它还能让你隐藏一些内部实现。有时你可能不想导出那些具体类型,出于各种原因。也许你确实可以返回那些类型……但是,返回接口有时来自于工厂模式的思维方式,因为有可能根据某些条件,它会返回不同的类型……你懂我的意思吧?在这种情况下你确实需要返回接口,或者你必须有几个方法并把逻辑移到别的地方。但有时,决定使用哪个类型的逻辑也是其中的一部分。
那么,反对这样做的理由是什么?是不是说返回具体类型更好,而调用者可以选择是否使用接口?
Jon Calhoun:其中一个理由是调用者可以决定是否需要接口。比尔提到的另一个理由是,在 Go 1.16 中有一个优化……我记得是与逃逸分析和额外的分配有关;我不记得具体是什么了,但基本上,它在内存效率上不如直接返回具体类型……不过这取决于具体情况。
如果我编写的所有代码都是我自己控制的,那么我觉得返回接口是没问题的,因为我可以随时修改函数和调用它的代码,不会有太大问题。但如果我在互联网上发布一个库,很多开发者都会使用它,有时返回接口意味着我可以在不引发重大版本变更的情况下进行一些本来会是破坏性的更改……对我来说,这值得牺牲一点性能。
Mat Ryer:这是个不错的点。那么标准库里有什么你想拿掉的吗?
Johnny Boursiquot:丹尼尔对此有很多想法……[笑]
Daniel Martí:我有一个想法,可能会非常有争议,或者完全没有争议;我不确定会是哪种情况……那就是 Plugin
包。
Johnny Boursiquot:哦哦哦!
Daniel Martí:因为我认为 Plugin
包的想法非常好,但它有点半途而废。它没有 Windows 支持,而且非常容易被误用……如果有人构建了一个插件并尝试与您的二进制文件一起运行,它几乎肯定不会成功。所以我认为这个想法很好,但它在完成之前不应该进入标准库。
Mat Ryer:是的……这个包的作用是通过这种奇怪的 Plugin
接口在运行时加载其他 Go 代码。有人看到过它被使用吗?或者自己用过吗?
Jon Calhoun:我好像没有。
Johnny Boursiquot:我也没有。
Daniel Martí:我见过人们尝试……
Mat Ryer:这说明了一些问题。我觉得这说明了很多问题。
Daniel Martí:是的。我认为如果你的目标平台只有 Linux,或者 Linux 和 Mac,我觉得它还可以。但如果它必须是可移植的,或者对所有 Go 用户都易于使用,我觉得它完全不适合。
Johnny Boursiquot:是的,我和你一样。这个想法本来充满了希望;举个例子,不能在运行时交换插件,这是一个很大的失误。我认为这是一个未完成的特性……但如果是未完成的,那也意味着它可以被完成并变得更加健壮。很多嘈杂声……它可以变得更加健壮。
我想人们不使用它的原因是它还不够好……还是说这是“先有鸡还是先有蛋”的问题?是不是因为它不够好所以没人用?如果它被完成了,人们会开始使用它,进而使插件流行起来吗?这很难说。
Daniel Martí:我认为如果它完成了,比如有了 Windows 支持,并且有某种包装器可以在加载插件时生成友好的错误信息,我认为对于那些加载一次就不需要卸载的情况,插件是可以的。但问题是,它已经在标准库中存在了三四年了,仍然没有被完成。所以我对它在短期内被完成不抱太大希望。而且现在你也不能真的移除它,因为一旦它进入了 Go 1,就不能再移除了。
Johnny Boursiquot:我们可以在 Go 2 中移除它,什么时候发布 Go 2 都行……
Mat Ryer:双关语过载,抱歉。
Jon Calhoun:当你创建一个新的库或语言时,我觉得总会有些东西会混进来,是你希望它们不在那里的……这听起来像是那种情况,它进入了标准库,现在如果你问 Go 团队中的任何人,他们可能会说“是的,它真的不应该在那里”。
Mat Ryer:是的……有时候是为了特定问题不得不解决的东西,标准库里有一些这样的例子。