SOLID原则

SOLID原则指五个面向对象的设计原则,每个设计原则的首字母拼起来,刚好是SOLID这个单词:

SRP: 单一职责原则。每个软件模块有且只有一个需要被改变的理由。

OCP: 开闭原则。软件系统应该允许通过新增代码来修改原有系统行为,而不是通过修改现有代码。

LSP: 里氏替换原则。实现某些接口的组件,必须同时遵守同一个约定,以便让这些组件可以相互替换。

ISP: 接口隔离原则。只依赖自己需要的部分。

DIP: 依赖倒置原则。调用方不应该依赖于被调用方的实现,而应该依赖于接口。


SOLID原则、设计模式适用于Python语言吗

阮一峰-代码的抽象三原则






20241117

帮我整理这一期英文播客,翻译为通顺的中文,请保留完整内容,不要删减,谢谢!

本篇内容是根据2016年9月份#82 SOLID Go Design音频录制内容的整理与翻译

Dave Cheney 本周参加了该节目,讨论 SOLID Go 设计、Go 中的软件设计、编写“好的 Go 代码”意味着什么以及错误处理。


过程中为符合中文惯用表达有适当删改, 版权归原作者所有.

todo

Erik St. Martin: 我们又回来了,迎来新一期的 GoTime。今天节目中有我,Erik St. Martin,还有 Brian Ketelsen 也在……

Brian Ketelsen: 大家好!

Erik St. Martin: 还有 Carlisia Campos。

Carlisia Thompson: 大家好!

Erik St. Martin: 今天,我们的特别嘉宾,虽然他其实无需过多介绍,就是 Dave Cheney。Dave,跟大家打个招呼,并简单介绍一下自己吧?

Dave Cheney: 大家好!我叫 David,是来自澳大利亚悉尼的 Go 语言爱好者。我参与 Go 已经很多年了,能有机会参与这个我非常热爱的语言项目,我感到非常幸运。大家好!

Brian Ketelsen: 让你自我介绍还挺有趣的。因为我们在 GopherCon 上发现,几乎所有我们聊过的人都是通过你的博客学习 Go 的。当时有大约 1500 人参加,结果其中 1499 人都提到了你的博客作为学习 Go 的资源之一。所以虽然介绍你有点多余,但我们觉得还是有必要的。

Dave Cheney: 是的,在 GopherCon 上遇到那么多热情的人真的很感动。大家都说,“哦,我超喜欢读你的博客!”真的让我很感动。五年前,我从没想过自己会成为作家、博主或公开演讲者。就像每个好工程师一样,你会想,“哦,我遇到这个问题太多次了!我可以通过把它写下来解决,这样就可以把链接发给别人。”所以我就是这么开始的。

Brian Ketelsen: 那么,有个问题——是 Go 促使你找到了自己的声音,还是成为更好的工程师让你找到了这条路?

Dave Cheney: 这有点像“是狗摇尾巴,还是尾巴摇狗”的问题。我大概四五年前加入 Canonical,当时我们被鼓励在 IRC 上沟通,所以我有机会在 Go 的 IRC 频道里“兼职”。当时 Go 1.0 刚刚发布不久,很多人有各种问题,他们会不断地来到频道里问同样的问题。正如我之前提到的,我试图通过写下解决方案来解决这个问题,然后把链接发给他们。与此同时,Go 已经支持 Arm,我知道这一点,然后我在澳大利亚找到了一台非常便宜的 Arm 设备。澳大利亚的神奇之处在于我们这里很难买到像 Raspberry Pi 这样的设备,所以当我找到这台机器时,我心想:“我能在上面安装 Go 吗?” 结果我成功了,这也成了第一个构建器。

我非常兴奋,想告诉大家,“嘿,你可以在这种简陋的 NAS 上运行 Go。” 这也是我写博客的起点之一。从此以后就一发不可收拾。

Erik St. Martin: 除了写博客以外,你还为 Go 做了很多贡献。

Dave Cheney: 是的,正如我在自我介绍中提到的,这是一个可以从零开始参与的机会。你所需要的只是一些空闲时间,从那以后事情就逐渐滚雪球般发展起来了。你知道,就是那种“看到需求,解决需求”的过程。

Brian Ketelsen: [04:01] 多年来,你在你的网站上为 Arm 平台提供了非官方的构建版本。我记得不久前,Go 才开始在 Build Dashboard 上正式支持构建器,并且开始在内部托管构建器。但在很长一段时间里,我们这些使用 Arm 板子的人都是直接从你那里下载 Go,这真的非常棒,我们所有的 Raspberry Pi 都非常感激。

Dave Cheney: 我真的觉得 Arm 非常特别。我知道 Intel 主导了大部分服务器领域,但 Arm 有着独特的魅力。它非常简单,指令集非常干净,而不像 Intel 的复杂性。就像我欣赏 Go 的简洁和极简一样,我也非常欣赏 Arm 的同样特质。构建 Arm 发行版的过程以及许多关于 Arm 机器的神奇之处在于它们通常比较弱,尤其是在 5.1 上运行的机器。很多机器没有足够的能力去构建 Go;交叉编译曾经非常困难,这是大家都知道的事。这就是我能做的事情之一。我联系了 Andrew,并表示,只要我在页面顶部放一个大大的警告,我很乐意维护这些构建版本,他也同意了。

在过去一年里,我们终于得到了真正的构建器,使用了像 Scaleway 这样的平台。我认为 Scaleway 托管了所有的 inter-builders。现在我们在 Dashboard 上有了真正的构建器,这意味着 Go 项目可以为所有人提供一个正式的 tarball,这正是我想要的结果。它已经从一个个人项目变成了 Go 团队完全支持的内容。

Erik St. Martin: 我最期待的博客文章恐怕是“Dave Cheney 如何在一天中找到更多时间”,因为…… [笑声] 你有一份全职工作,你为 Go 贡献代码,提出语言变更的提案,最近你还经常旅行,在很多 Go 会议上发表演讲。你怎么能找到时间做这一切,简直令人惊讶。

Dave Cheney: 是的,确实日程排得挺满的。来自澳大利亚的“魔法”就是不论你去哪里,飞机都得飞一两天。我不知道,我一直都觉得自己很幸运。Canonical 非常支持我从事 Go 工作,他们甚至赞助我和 Arm 一起开发了第一个 Arm 64 板子的初版。能够把我喜欢的事情变成日常工作,真的是太棒了。而且你总能在周末或下班后找到时间,把它融入到生活中。我真的很幸运,能做到这一点。

Brian Ketelsen: 代表全球所有 Go 社区的人,我们真的非常感谢你为推广 Go 所做的努力,感谢你教会了大家,并帮助构建了这个社区。我认为 Go 社区如今的模样,离不开你的贡献,所以谢谢你。

Dave Cheney: 谢谢你,Brian。

Carlisia Thompson: 我完全同意。

Erik St. Martin: 我也得把自己的名字加到那份从 Dave 的博客中学到很多东西的名单上,尤其是在早期。Brian 和我刚开始接触 Go 的时候,大约是 2011 年底到 2012 年初,当时几乎没有太多的学习资源。

Dave Cheney: 我感到非常幸运,也很荣幸能够做到这一点,因为那里有许多我可以填充的空白。有很多关于 Go 的好东西值得分享,我学到了很多,也想告诉别人,并且有机会这么做。

[07:55] 如果你五到十年前见到我,你绝对不会认为我会做这些事情。我不知道我是被迫学会了这些,还是幸运地学会了成为一名公开演讲者。我高中毕业后就再也没有做过公开演讲。我当时想,毕业后我可以把这一部分的技能丢掉了,“我再也不需要做公开演讲或辩论了”,但后来我不得不重新学习这些东西。至于写作?我的父亲对我现在的写作非常满意。他常说,“你的写作水平进步了很多。”

作为一个工程师,刚从高中毕业时我曾想,“非小说类写作?谁需要那个?我们只需要代码。”

Brian Ketelsen: [笑] 我们现在已经花了 14 分钟在“Dave Cheney 崇拜”上,所以我们先暂停一下“拍马屁”,聊聊你最近做的那些很酷的事情吧。你上周在 Golang UK 做了一场关于稳固设计的精彩演讲,能跟我们聊聊这个话题吗?

Dave Cheney: 那个演讲的核心——稳固设计原则——源自几个月前我在 Perth 的一次演讲,那次是在 Dave Thomas 的 YOW! 会议上。当时的听众是不太了解 Go 的一群人,大多数都是对函数式编程非常热情的技术人员。所以我有机会把这个演讲重新调整,讲给“自己人”听。我想让它对听众有趣一些,解释为什么你应该关心这些原则,而不仅仅是展示一个抽象的概念。我希望将其置于一个更大的议题中。如果 Go 想成为一种语言,企业愿意在上面投资,愿意用它来构建产品并将其用于基础设施中,那么他们可能会在接下来的 10-15-20 年内继续使用它。

我更大的信息是,Go 程序员需要开始思考程序的设计,因为否则我们可能就会成为“下一个 JavaScript 框架”,然后可能会被其他东西取代。但如果我们开始关注设计,甚至更重要的是,开始讨论设计,发展出一套讨论程序设计的语言,而不仅仅是从一个热门的 HTTP 框架跳到下一个,那么 Go 就有机会成为一种人们愿意在 10-15 年后仍然使用的语言。

Erik St. Martin: 所以 Dave 肯定听过我们的节目,因为 Brian 差不多每期都会抱怨 HTTP 路由器的数量。 [笑声]

Dave Cheney: 我在 Golang UK 还做了一个关于如何编写更快 Go 代码的演讲,所以我可能有点虚伪,批评那些写 HTTP 路由器的人。但更大的问题是,我们今天编写的 Go 代码必须是可维护的、可变更的,不能只是为了解决当下的需求。它需要是长期可维护的。

否则,可能五年后人们会说,“哦,这个 Go 代码……它不可维护。我们该怎么办?” 然后他们会用其他语言重写它,继续寻找“下一个热门语言”。如果公司要在一个项目上投资十年或更长时间,那么程序的维护性比仅仅拼凑原型并看看它有多快更为重要。

Carlisia Thompson: [11:38] 设计还有另一个层面。对我来说,要考虑设计确实需要付出努力;你需要花时间去学习什么是好的设计。但一旦你开始学会它,编码其实会变得更容易。比如,当我开始学习 Go 的时候,我会想,“我应该把东西放在哪里?” 我看到很多人到处问这个问题——“我的模型应该放在哪里?这个应该放在哪里?那个应该放在哪里?” 但如果你花点时间思考如何设计你的接口,如何设计你的包,如何组织一切,如果你开始思考依赖关系——这些问题就会自动得到解答,你最终会写出好的代码。

我认为,思考设计有多个层面和多个理由。

Dave Cheney: 你说得非常正确,而且你提到了一个非常重要的点——你刚才说了“好代码”,但这是一个非常主观的概念。什么是好代码?我喜欢代码看起来某种样子,我喜欢用长标识符,我喜欢把接收器命名为这个,而不是我们通常习惯的单个字母,因为这是我以前的做法。我在英国的演讲中想讨论的是,如果我们有一种不同的语言来讨论设计,而不是那么主观的语言,比如“我喜欢这样,我偏爱这样”?我们可以有一种更客观的语言。这正是 Martin 的稳固原则所探讨的内容;它们并不是不带偏见的,但它们来自于一种观点:“我喜欢这个,因为它易于修改。我不喜欢那个,因为它与其他类型的耦合太紧密了。” 我们可以更客观地进行讨论。

Martin 关于稳固原则的说法是它们不是规则,而是指导方针。你可以说,“我应该在所有事情中都诚实,我应该对我的朋友坦诚”,但有时候你不得不稍稍违反规则,讲一些善意的谎言,这类事情。当你谈论设计原则时,你可以说,“我不喜欢这个,因为它耦合得太紧密了,但为了实现我们的目标,这似乎是一个合理的妥协。”

用这种语言来讨论设计很有趣,而不是说“我不喜欢那个。代码应该更漂亮一些”,或者“我不喜欢这个,代码应该更短一些。” 这些说法在一个更广泛的设计背景下并没有太多可操作性,因为每个人的想法都不同。“多短才算短?多漂亮才算漂亮?” 这些变成了主观的,进而引发争论的话题。

Erik St. Martin: 对。有些人指出的那些问题更多是从美学角度出发的,就像艺术一样——我欣赏的东西和你欣赏的东西总会有所不同,但我认为我们都可以同意高度耦合的两个组件是难以维护的。我们可以在这点上达成共识。我认为,从这个客观层面来讨论问题是非常有意义的。这也是为什么几年前我进入 Ruby 世界时非常喜欢 Ruby 的原因之一,因为当时人们总是在讨论这些问题,比如 Demeter 法则、耦合与内聚等等。

我很欣赏人们讨论如何创建干净的抽象和可维护的代码,所以我很喜欢我们在 Go 世界中也开始讨论这些话题。

Dave Cheney: 是的,完全正确。我认为这是至关重要的,因为如果公司要进行长期投资——而且他们已经开始了。我们有 Docker、所有 CoreOS 产品、所有 HashiCorp 产品、Kubernetes——这是一个投资。但为了让这项投资在未来得到回报,不能只是尽可能快地编写代码,必须有一些基本的设计,这样我们才能在未来需求变化时修改代码。

Brian Ketelsen: 我的问题是,随着 Go 逐渐成为一种更受欢迎、更成熟的语言,是否存在一个成熟模型?这是否意味着我们必须经历一些丑陋的成长阶段,比如 Gang of Four 模式和企业 JavaBeans?Go 的成熟模型是什么样的?

Dave Cheney: [16:09] 在我思考时,有没有其他人想先说?

Erik St. Martin: 当然。我认为 Gang of Four 可能有点过了。我们不希望走到那一步,因为 Go 这门语言从设计上就有意避免了这种模式——比如继承链等等。但我认为我们可以从中学到一些经验。大多数这些抽象都是出于合理的原因而构建的,抽象化、创建干净的接口等等。我认为我们可以做到这一点,而不必依赖那么多的模式。

就像 Dave 说的,我们应该有明确的目标,而不一定要遵循固定的模式。这是我的看法。

Dave Cheney: 我在演讲中有一个部分,但由于时间不够,我删掉了,主要是因为对此部分的反馈很多。我在演讲开始时问,“谁做代码审查?为什么要做代码审查?” 有人喊道,“为了找出坏代码。” 这就是引子。我继续说,“那么模式书——它教你如何编写好代码吗?” 我的观点是,“也许不是。也许从更大的背景来看,它们被称为模式,因为你像使用缝纫模式或食谱一样应用它们。当你遇到特定问题时,你可以使用一套适合的解决方案。” 但我认为讨论设计和原则——借用 Martin 的词汇——是更高层次的概念,关注“目标是什么”,而不是“我现在要解决的问题是什么?” 如果目标是让代码可维护——我的意思是可以在未来修改,像访客模式或迭代器模式这样的东西不会给你讨论这个问题的词汇。那只是一个针对具体问题的解决方案。

Erik St. Martin: 是的,而且我认为这也限制了可能性。如果我们总是说“你需要使用这些模式中的一个”,我认为这也会封闭创造力。但我们可以从更一般的层面来讨论问题。比如,这两个组件不应该耦合在一起。

我总是把两本书搞混了……有另一本书叫《Clean Code》是 Bob Martin 写的,还有一本是《程序员修炼之道:从学徒到大师》,我非常喜欢这两本书。它们有很多独特的想法。它们展示了如果某个类型似乎对另一个类型了解太多,也许你把函数放错地方了,诸如此类的东西。

我们可以在非常高的层次上讨论这些问题,而不必深入到“这是一个访客模式”或者“这是一个装饰器模式”,或者 Gang of Four 书中的任何其他模式。

其实挺有趣的……自从用 Java 以来,我已经好多年没思考过 Gang of Four 的书了。顺便说一句,这也是一件好事,因为我清楚地记得有一段中间件代码,我发誓有人从书里找了每个模式的用例,并应用到了那段代码上。 [笑声]

Dave Cheney: Gang of Four 模式书有点奇怪。我猜它是 80 年代中后期到 90 年代写的……当时,人们——我指的是那些先贤们——认为这只是众多书籍中的第一本,认为每年都会有新的模式出现,2005 年的模式、2006 年的模式,源源不断。但事实证明,书中那三十来个模式差不多就是所有的了。

[19:52] 这并不是因为 Demarco 和他的朋友们(我想是 Gamma)坐下来想出了 30 个模式,然后他们说,“好吧,我们要出第二版书了。” 他们是通过观察他们看到的代码得出的。他们有点颠倒了顺序——他们审视了大量的代码,试图找出其中的共性,然后从中推导出了一本模式书。但他们再也找不到更多的模式了,因为它们是有限的。它们就像自然法则——你不能发明新的自然法则,它们只是存在。

这实际上把模式书设定为一种描述所有软件设计的起点;它成了那个时间点上关于软件设计的几十个观察结果,这些观察是在 80 年代写下的。

我的演讲中有一个启发我很深的部分,讽刺的是我是在伦敦受到启发的,因为 Jim Warrick 2007 年的演讲开头谈到了伦敦大火。他说,当他们要重建伦敦时,有很多关于如何重建伦敦的提议,很多问题……他在演讲开头问的是,“看看这些不同的重建伦敦的计划,如何决定哪个是好的?看看这个房间里的所有建筑师,如何决定你喜欢哪个?什么是正确的设计来重建伦敦?” 同样,没有语言来描述什么是好的建筑设计,就像没有语言来描述什么是好的软件设计。

Erik St. Martin: 是的,这很有趣。我们也可以对算法说同样的话,对吧?有很多明确定义的算法有非常好的用例,我们根据需要从这些定义良好的算法中选择,但有时基于我们对事物如何扩展、它们的性能以及它们的简洁性的了解,我们也可以自由地做出自己的选择,对吧?

Dave Cheney: 是的,说到算法——我们讨论算法的元语言是什么?是大 O 表示法,是时间复杂度与空间复杂度的比较吗?链表具有更好的空间复杂度,但时间复杂度较差,而且在当前的硬件上,它的时间复杂度甚至更差,相比之下,向量要好得多。所以讨论算法的元语言是时间复杂度和空间复杂度的概念;这是空间与时间的权衡。

Erik St. Martin: 不幸的是,这比我们现在讨论的这些概念要更容易衡量,比如耦合程度——这很难衡量。

Dave Cheney: 是的,是的。但仍然可能会有争论……你可以将哈希表与仅仅是一组项目的数组进行比较。对于非常大的哈希表和非常大的项目数组,查找时间是 O(log N) 对 O(n)。但对于非常小的表,N 非常小,所以它无关紧要。这些是微妙之处……如果你说,“我们总是必须使用哈希表,因为哈希表查找速度更快”,那就忽略了事实……比如 HTTP 有一个映射——它里面会有五个东西吗?设置这个哈希表、对所有项目进行哈希的开销是多少?与直接线性搜索相比如何。HTTP 头通常只有五六个项目。

所以这些是可以用空间和时间、大 O 表示法讨论的设计决策。这就是我想让 Go 社区的人们开始讨论的——从更高的层面来讨论设计,而不仅仅是在 Reddit 上发帖说,“嘿,哪个框架最快?最好的 HTTP 框架是什么?” 我想看到人们开始在更高的层面上讨论,开始思考“什么是设计我的应用程序的最佳方式,使其在未来易于维护?使其可维护、可重用、可组合。”

Erik St. Martin: [24:06] 是的,我也很想看到更多这样的讨论。我已经开发 Go 有好几年了,但我仍然在为包布局问题苦恼。什么时候应该把东西分成不同的包?什么时候应该创建子包?像我们有 ioioutil 这样的东西。我仍然在从常见代码库中提取这些模式,并制定自己的经验法则。像这样的讨论越多,越多的人参与讨论,我认为是非常好的事情,因为我认为很多人可以从中学习。

Dave Cheney: 是的,标准库是把双刃剑。对于像你这样的作者,或者任何想提供不会在下周就过时的建议的人来说,标准库非常好,因为它会在未来五到十年内保持不变。我们可以给出这些建议,并且知道它不会很快过时。但关于 Go 库的问题是,虽然它是世界上审查最严格的 Go 代码之一,并且是当时最优秀的 Go 程序员编写的,但它也是最早期的一些 Go 代码,并且存在一些不一致之处。你可以从中找到几乎任何支持你观点的例子。无论你想证明什么论点,总能在其中找到支持的例子。

这很棘手,因为我们告诉新 Go 程序员的三件事之一是什么?去做 Go Tour,去读语言规范,去读标准库。这并不是我们希望的那种明确的指导。

Brian Ketelsen: 标准库的部分问题在于,其中一些代码是在我们还不知道什么是好的 Go 代码设计时编写的。它随着设计的发展而演变,而我们——Go 社区——出于各种原因没有花时间回过头去修改标准库中的那些东西。所以我同意,看到标准库中由于代码年龄差异而产生的风格差异确实很有趣。

Dave Cheney: 是的,完全正确。

Erik St. Martin: 是的,你提到的这些变化和演变是基于当时我们所知道的东西。你一直在做的一个演讲主题并倡导的是错误处理。我认为在我们转到这个话题之前,我们应该稍作休息,感谢一下我们的赞助商,然后我们再回来开始讨论错误处理的变化。

Dave Cheney: 好的,没问题。

插播广告:[\00:26:43.01]

Erik St. Martin: 我们刚才讨论了标准库及其如何随着我们在 1.0 规范下编写代码的多年经验而演化。我们现在开始看到的一些东西,其中一个你主要倡导的话题就是错误处理。过去一些 sentinel 错误值和简单地返回错误似乎已经足够了,但我想深入探讨一下你的新方法。你在 GopherCon 上做了相关演讲,并且你还有一个帮助处理错误的包。我想听听你的看法。

Dave Cheney: 当然。首先,在我说任何话之前,我必须非常清楚地说明,我只是在许多前人的肩膀上稍微站得高一点。我写的 errors 包直接受到我们在 Canonical 为 Juju 写的包的影响,而该包又基于 Roger Peppe 早期的工作。这种想法有一个很长的演变过程,这也是我在 Go 的其他领域看到的现象。不想太分散注意力——例如两年多前,Rob Pike 写了一篇关于函数选项的文章。我认为这是一个非常了不起的模式,所以我在 dotGo 上谈到了它,主要是因为我觉得人们没有给 Rob 的帖子足够的重视;我认为这是一个很棒的想法,但它有点被忽略了。

我站上舞台,在 dotGo 上谈论了它。这种想法不断渗透,最新的演变是在 gRPC 中——他们在所有 gRPC 类型的构造函数中大量使用了这种方法,正式化了选项的命名以及他们将如何调用它。我看到的更广泛的现象是,这些想法正在不断演变,这正是应有的。

谈到错误处理——这是两件并行的事情。第一个故事是,我写了很多关于我认为应该如何进行错误处理的博客文章,这与堆栈跟踪等无关。事实上,尴尬的是,我实际上写了两次相同的博客文章,相隔一个月。我甚至给了它们相同的标题。当它出现在 Reddit 上时,人们以为是交叉发布。总之,我的核心观点是,我在大力推动这样一个想法:如果你的函数返回了一个错误——通常情况下,当然总有例外,特别是当你处理网络和可重试的事情时——你应该尽量说,“这个错误发生了,我对它的细节一无所知。我只需要执行我的清理操作,然后将错误返回给调用者。” 这样做使代码和设计变得非常简单且解耦,你只需说,“发生了一个错误。我正在清理我在这个函数中做的任何事情”,大多数时候几乎没有什么要清理的,因为我们有精心分解的小函数,然后你只需将错误返回给调用者。“我不知道发生了什么。我清理完了,现在将错误返回给调用者。”

我曾讨论过的一个想法是,与其检查错误是否匹配某个特定值,或者错误是否是特定类型,你应该试着思考,“好吧,如果我需要知道这个错误的某些信息——它是临时错误吗?它是否符合临时接口?(你可以从 net 包中获取,或者自己定义)。” 这引导出了一种松散耦合的错误处理方式。我不知道这个错误发生在哪里,也不知道它是我的直接调用中发生的,还是在数百个堆栈帧下发生的。发生了某些事情,我将清理并将错误传递回去。

[32:06] 这是一种设计 Go 程序的方法,以这种快速失败的方式处理错误。在每个阶段,你都不需要说,“哦,出了点问题。我要看看这个错误是否匹配我知道的几十种情况,在那些特定情况下我要重试,或者我要调整某个计时器,然后再试一次。” 不,不要这样做。直接崩溃。直接崩溃,把它返回给你上面的人,也许他们知道该怎么做。把错误传回去。快速失败,因为我们知道这是编写可靠软件的方式,这个想法叫做“仅崩溃软件”。如果发生错误,就退出;某些东西会重新启动你,然后再试一次。这是其中的一部分。

Erik St. Martin: 在检查是否是临时错误的情况下——我知道你在演讲中提到了这个——你是建议如果错误是临时的,人们应该进行某种指数退避,带有一定的反压,最终失败?还是你建议总是返回并将错误传递上去,在最高级别处理?

Dave Cheney:这总是一个权衡。我认为在那些代码确实知道它在处理网络的地方——这回到了你希望有模块化设计的概念;模块之间的交互是通过接口,而不是具体类型的。在你的代码确实知道它在处理网络的情况下,比如你正在写一个HTTP服务器,或者你在写SSH包,或者类似的东西——这些大多数情况下都实际上会与网络交互。你至少知道,当某个特定的调用操作失败时,可能是因为DNS故障,或者网络出现了波动,或者类似的情况。

通常,当你在处理二进制包时,被传递了一个ReadWriterReadWriteCloser,你并不知道它的来源——它可能是一个缓冲区,可能是磁盘上的文件,可能是任何东西。这真的取决于你的包的目标是什么,并且你要承担起这个责任并将其封装。你可以想象HTTP包对网络有很多了解。甚至在1.8版本中,我们还在尝试在认为安全的情况下重试HTTP操作。比如一个GET请求,没有请求体,建立连接时出现了临时错误。在这种情况下,可能可以重试,但所有这些附加条件意味着你对该包运行的环境非常了解。

我认为,通常情况下,你并不知道你的包是如何运作的。很多时候,或许你也不应该知道;或许你应该尽量将它们视为黑盒,因为这样它们更容易组合在一起。代码之间的隐式协议更少,它们必须是显式的,否则它们就是不透明的。

Erik St. Martin:我猜这很有道理……因为即使你从网络的角度考虑——你在做什么?可能发生了某个临时错误,但你不能保证幂等性,对吧?你可以重试,但这可能会在另一端引发一些意想不到的影响,因为操作可能部分完成了,或者类似的情况。重试总是很难;你真的需要了解系统在做什么,可能还要检查状态,确保在重试之前没有发生半提交的情况。

Dave Cheney:[35:46] 正是如此。在这些情况下,不应该那么轻易地盲目进行重试。你或许应该考虑这个操作是如何失败的。这意味着你需要非常熟悉下游代码的所有部分,这也意味着更多的耦合,你对构建在其之上的组件有了更多的了解。当然,也有适合这样做的情况;我认为这些情况比人们想象的要少,一般来说,你应该尽量将你的程序由小块组成。

举个例子,SSH包,它是基于网络和公钥、SSH代理构建的……这些类型实现的接口只是普通的read/write closer。我们非常努力地使con接口和session接口看起来几乎像read/write closer,或者类似于你从os/exec中得到的那种东西。人们不期望os/exec的阶段是可以重试的,所以我们也不暴露这些内容。这都是从构建高层次互操作包的角度考虑的。除了接口,它们彼此之间几乎不了解对方。

错误处理是另一个独立的部分,当错误发生时,你如何告知开发者或运维人员——正如我之前所说的,你只是挥挥手说:“我只会将错误返回给上层代码,它会知道如何处理。”

最终,你会到达函数的顶部,或者是你的web服务器的主处理器,或者其他地方,这时你需要弄清楚到底发生了什么。在这种情况下,你希望尽可能多地获取有关错误的信息。你想编码尽可能多的信息;最好你能获得堆栈跟踪或其他东西,指出错误实际发生的地方。因为作为开发者,我会收到一个错误报告,如果它只是说“请求失败:io.EOF”——那是从哪里来的?

错误处理的第二部分是利用错误作为一个值的事实。我们刚刚从调用者的角度讨论了它——只是把它变得不透明,只是说“发生了一个错误”,但你不知道除此之外的任何信息。然后我们可以利用这一点,将额外的信息嵌入其中。长久以来,传统做法是使用fmt.Errorf,加上一个前缀,然后打印出错误。然后一路上注释错误,你会得到一个字符串,每次在前面加一点内容。

这种模式在标准库中大量出现,Donovan和Kernighan在他们的书中也提到过……有很多Go代码的写法是“if err != nil { return fmt.Errorf}”,一些描述了发生了什么的注释,然后是错误的文本。 [\00:39:06.14]

这很好,因为在顶部你会得到Roger所说的“面包屑”(breadcrumbs),比如“这个失败是因为那个失败:因为这个失败:因为那个失败”,你可以通过grep这些字符串,手动构造出你在代码中的堆栈跟踪。

这很好,但也有一些问题……有些情况——虽然很少,但我希望它们不存在——在标准库中,你确实需要检查一个特定的值。io.EOF是一个典型的例子。任何I/O读取器都必须返回io.EOF。它不能返回ReadFile:io.EOF,它必须返回完全相同的io.EOF值。在这种情况下,我们实际上是在检查相等性。

[40:06] 在某些情况下,你不能进行这种注释,因为如果你将io.EOF打印出来,加上另一个字符串,然后返回一个完全不同的值——比如通过fmt.Errorf——你会得到一个无法比较的结果,无法再剥离掉这个前缀,因为你已经永久地损坏了它。所以如果我们谈论的是使用错误值来注释额外的信息,比如一些消息、堆栈跟踪或类似的东西,那它必须是可以撤销的。同样,我在这方面的工作很少,而且绝不是独一无二的。我建立在很多已有的工作基础上,这个想法是:“好吧,如果我们有一个错误,让我们给它一个方法,让你获取底层错误。”

如果我们将它们一个接一个地堆叠起来,那就需要一个方法让我们可以撤销这个堆叠,这样如果我们确实需要这种行为来检查“这是io.EOF吗?”或者例如你使用“OS does not exist”之类的东西,它知道来自syscall、Windows等的一些特定类型;它知道如何检查这些类型,并说:“我知道如何解释这些错误类型。我知道如何查看它们并判断是否确实是由于文件未找到引起的。”所以你需要能够——无论你做了什么包装——添加上下文、堆栈帧、消息,并且你需要能够撤销这些操作,因为有些情况下你需要提取错误,因为这是代码的运行方式。

Erik St. Martin:对,我们确实看到有些情况下,人们将基于它们返回的错误类型屏蔽了哨兵错误值。依赖这些哨兵错误值会变得有问题。

我喜欢这种方法的另一点是,我们看到人们尝试解决这些问题的另一种模式是使用标记日志,但这只对发出去的日志消息有帮助;这对返回到堆栈上流的调用者没有帮助。

Dave Cheney:哦,这涉及到我的另一个大话题,那就是只处理一次错误。处理意味着基本上我已经检查了错误值;如果error != nil,那就是你的检查,我已经看过了。然后你只能做出一个决定。这个决定可能是记录日志;你已经将它写出来了,因此错误已经被处理了,你不需要将它返回给调用者。

现在,哪些情况下你可能会记录日志呢……假设你在搜索路径中查找某个文件。它不在你的主目录中,也不在共享目录中,也不在系统目录中。你不会在第一次失败时就退出;如果它不在你的主目录中,你还会查找共享位置,然后查看系统位置。所以你检查错误,可能会发现“好吧,那里没有找到,但我还有两个搜索路径可以查找”,所以错误在那时就被处理了。

我经常看到的是,在调用栈的每一层,如果error != nil,我会写出“日志——发生了一些错误”,然后将该错误返回给调用者。这意味着无论你在代码中应用这种模式的次数有多少,你都会得到10或15条不同的日志消息,基本上都在告诉你同样的事情:“发生了错误——发生了错误,失败了。发生了错误——无法打开文件。发生了错误——无法解析JSON。”然后,在你的处理器或主函数的顶部,你只会得到原始错误,没有堆栈跟踪,也没有上下文。日志记录通常发生在外部;它被写出到STDOUT,或者通过某种日志传输,而实际处理失败情况的程序逻辑则在一个完全不同的世界中运行。因此,你不仅生成了大量日志消息,而且你在程序顶部得到的内容没有任何上下文,也没有与日志上下文的任何联系。

[44:11] 我强烈主张,如果有错误,只需将其返回给调用者。错误包中的wrap方法让你能够将你原本要通过log.error发送的那一点点日志上下文放入错误本身(Errors.wrap),并将其返回给调用者。这样你就得到了之前通过这种侧信道发送到日志文件中的所有注释,现在它们可以在顶部使用。当你要打印或分析该错误并将其记录到文件中,或者因为发生了错误而退出程序时,你拥有所有这些上下文,供操作人员或开发人员弄清楚发生了什么。

Carlisia Thompson:我有一个问题。结构化日志如何与这种理念相契合?我认为我看到你所说的观点,但如果你没有认真编写日志消息字符串,如何处理重复的日志?你如何在想要进行结构化日志记录时处理这些问题?你是使用累积到一个值中的层级转储作为日志吗?

Dave Cheney:这可能是我最主观的观点,但我不认为结构化日志有多大意义。并不是说它没有用,而是与随意的文本记录相比,这种键/值对的概念确实有用,但我认为日志有两个主要使用者——一个是运行该程序的人(通常是操作员,因为我来自运维背景)。在这种情况下,如果一个程序告诉我一些事情,就像UNIX哲学——如果程序告诉你一些事情,那它是重要的,你应该注意它。我工作过的环境中,有很多情况下你无法查看日志消息而不先通过grep过滤掉一堆无用信息,这就是个大问题。

所以如果你记录的唯一内容是用户需要采取行动的内容,那么我看不出为日志描述键和值框架的价值。如果非常明确,这是我的个人意见;我不想强加给其他人。我知道很多人认为结构化日志非常有价值,但从我的角度来看……如果你要记录一行日志,实际上是你需要采取行动的事情,那么可以说你应该尽量少记录日志,因为如果有数千行输出,说明出了大问题。如果有数千行输出,而没有任何问题,程序只是过于冗长,那么你有一个更严重的问题。

第二个使用者显然是开发者,我认为他们与操作员(即运行程序的人)和调试程序的人是分开的。开发者希望所有日志和跟踪都打开,所以在那种情况下,结构化日志是非常有用的,也许可以解释为总是打印出文件、行号和执行的函数,也许还包括一些时间戳之类的东西——那是非常有用的。但我认为这两种用例不应混为一谈。

作为操作员,我只希望程序在有需要我采取行动的情况下输出内容。它不应该只是告诉我它还在运行,它不应该告诉我“无法拨号,但我在重试。别担心,一切正常。”这些都不是我需要关心的,你不应该告诉我这些。对于开发者来说,是的,你希望打开所有这些日志,这样你可以看到重试循环,并说“好吧,它总是重试三次才做任何事情。”但它们是不同的使用者。在这种世界观下,结构化日志对我来说似乎没那么有用。

Erik St. Martin:[48:15] 我们与Netflix的Scott Mansfield进行了一次对话,他谈到了他们如何不太依赖日志,而更多依赖计数器。他们使用度量来监控一切;他们会计算重连错误的次数,并在一段时间内测量这个值,如果看到重连尝试的频率比平时高得多,他们就会意识到有明确的问题,开始调查。

我认为另一个人们喜欢结构化日志的场景是分布式应用程序跟踪。我可以查找标记为“请求ID”的日志,并获取与给定请求相关的所有日志。但正如你所说,当规模变大时,管理所有这些日志真的很难,所以你不得不找不同的方法。

我想回到我们刚才讨论的,尽量将错误返回上层。我看到很多人使用日志记录的一个场景是在某种选择循环中;你从通道中拉取内容,发生了错误,你不想返回,因为你只是一个并行工作流,你并行于主线程运行,所以你不想死掉,因为那样你就会停止处理所有消息。我通常在这些方法中看到大量的日志记录,这样人们就不会简单地丢弃某些事情出错的事实。

Dave Cheney:是的,你要考虑你所处的角色。你是开发者角色吗?“我想观察这个选择循环的操作。这是同时运行的许多循环之一;我相信如果我能深入了解这些相互交织的部分是如何运作的,我就能在事后重建事件流。”这是开发者角色。

操作员角色,如果它只是不断输出信息,比如“我又回到了选择循环中”,“触发了哪些事件条件?”那会让我非常愤怒。我曾在交易公司工作,我们会为每个应用程序生成数GB的日志——当时有很多应用程序在运行——我整天都在压缩和解压这些日志,然后用grep过滤掉一堆无用的信息。我认为分布式跟踪的例子确实很好地反驳了我对结构化日志的看法,实际上,是的,请求ID是你想要贯穿整个流程的东西,但这是否需要打印在控制台上,比如“我正在处理这个请求”,还是只是记录到……?

就日志记录而言,肯定有一些例外情况。审计日志可能是一个完美的例子,说明结构化日志的用处。用户ID,组ID,权限设置,操作完成。因为在逻辑系统和良好监管的系统中,你确实需要审计日志。不过,审计日志并不是操作员会一直盯着看的东西。你不会因为审计日志中发生了一个错误而触发警报。它们是不同的角色。

Carlisia Thompson:关于同样的话题,我想问一下你的意见,因为我认为日志记录和指标化的度量服务于不同的目的。例如,我可以开始跟踪某个请求通过的次数,因为我通常每天得到50万个请求,如果突然降到20万个,我想得到警报,提醒我有事情正在发生,我想查看这些指标。你对在代码中引入这样的指标提取有什么看法?

Dave Cheney:[52:09] 是的,它们绝对是不同的东西。日志记录是为人类服务的;度量是为机器服务的,它为你的监控、自动化警报、自动重试、扩展和缩减提供服务。如果你是通过跟踪日志文件来驱动这些过程,那你有一个严重的运维问题。这两者是独立且不同的。

Erik St. Martin:是的,我也认为日志记录应该是你可以逐步减少的东西。我曾在一些系统上工作,通常它们通过UDP流传输;你不希望日志记录因为磁盘上的某些延迟而减慢你的应用程序,或者因为磁盘空间不足而导致系统灾难性失败等。

我猜这取决于你的日志有多重要。如果你是在为银行做一些事情,你可能希望记录每一条消息,对于审计来说这可能很重要。但在其他情况下,如果你在记录网站的请求,如果因为某些延迟而丢失了一分半钟的日志,那也不是世界末日。

Dave Cheney:是的,它们是不同的用例——审计日志,HTTP请求日志,如果你需要保留它们用于分析、欺诈检测等……然后是你的实际应用代码中的日志——每次它跟你说话时,是在告诉你“这是你需要关心的东西”吗?还是只是在告诉你一些你已经习以为常的东西?我的经验法则是,如果要在应用程序上工作,你必须用grep过滤掉一堆对你不重要的东西,那么问题是你记录了太多日志,而且这些日志的价值不够。如果这与必须进行的审计日志完全分开——HTTP请求日志必须进行,用于分析和欺诈检测,或者用于监控系统健康状况的度量。

Erik St. Martin: 这真的很难,因为没有一条绝对的规则,对吧?就像我们今天讨论的其他话题一样……这关乎于你如何看待编程,决定这些东西对你来说有多重要。它们是操作和维护这个应用程序的必要条件吗?还是说它们只是一些让你觉得放心的小细节,比如你可以打开日志查看数据,但实际并不常用。我见过太多应用程序在日志记录方面做得很重,但几乎从没人去查看日志——这可能是我参与过的大多数应用程序的一个共性。大多数情况下,你只是查看你的指标仪表盘之类的东西。要在日志中逐行搜索太难了,尤其是当你没有一个集中的日志存储时。

在大规模系统中,这也会演变成一个问题,因为分布式日志记录系统本身也可能会失败,你需要做出决定,这种额外的复杂性和存储所有这些日志是否值得,还是你只想大致了解应用程序的状态。

Carlisia Thompson: 那些为你处理这些问题的公司收费非常高,这非常昂贵。

Dave Cheney: 是的,没错。


Erik St. Martin: 这些公司特别喜欢你向他们扔一些不必要的日志。[笑声] 通常是按某种费率计费的。

Dave Cheney: 这对他们来说是一个强烈的道德风险,因为他们没有动力帮助你改进。他们只会为你开发更好的工具来处理更大体量的日志;他们会说,完全没必要改变,继续保持现状吧。

Erik St. Martin: [55:53] 我认为困难在于,当你查看一行代码并思考“如果这里出了问题怎么办”时,你会联想到所有你曾经调试一个应用程序但信息不足的情况。我认为人们往往会倾向于提供过多的信息,以防自己需要它。我猜除了总是记录日志之外,也有其他解决方法。

有些人会构建一种方式,让他们能够向应用程序发出信号,以改变日志记录级别的详细程度;有些人会做 Canary 构建(试验性发布)时加上额外的日志记录……有很多方法可以用来调试问题。

另一个问题是当你遇到错误时——这是一个持续存在的系统性问题,还是只是偶然发生的异常?你偶尔会遇到 bug;无论你放入多少日志,可能永远也无法弄清楚为什么会发生。如果你无法重现它,单靠查看发生时的日志消息总是很难调试。日志消息的真正作用只是引导你重新建立出错时的条件。

Dave Cheney: 完全同意,一旦问题发生了,已经太晚了,马已经跑了。回到错误这个话题——你提到了错误。我认为 Go 在作为编写服务器软件的语言方面如此成功的原因——这也是我们真正看到它的核心应用领域,虽然它也扩展到了其他领域,但它的主战场是服务器软件——是因为我们处理错误的方式。我们没有异常处理机制。也许不是每个人都意识到了这一点,但每次你敲下代码时……你可能会想,“哦天哪……如果 error = nil,那就返回 nil。我每次都得写这个……”——但这其实是忽略了重点。它让你思考,如果这个操作失败了会发生什么,你必须在整个 Go 代码中每次都这样做,因为我们没有异常处理机制。而我们没有异常处理机制的原因是:为了编写可靠的程序,你必须首先考虑失败的情况。

不要担心顺利执行的路径,想想当事情失败时会发生什么。我认为这正是 Go 在编写服务器软件时如此成功的原因,因为你不能简单地以线性的方式编写代码,假设“一切都会顺利进行”,“抛出一个异常,会在函数声明中处理所有问题”。我们看到了有异常处理的语言在可靠性方面的表现;你永远不知道它们什么时候会爆炸。而尽管 Go 的错误处理可能显得有些繁琐,但它让我们在每个函数调用时都考虑“如果失败会怎样”,因为任何函数调用都可能失败。如果你不希望某个函数调用失败,那就不要返回错误。通过添加额外的前提条件来确保它不会返回错误,或者接受这样一个事实:只要你处理现实世界的事物(网络、磁盘),它可能会失败,你需要在错误发生的那一刻处理它,而不是简单地挥挥手说“IO 异常会传递给某个知道如何处理它的人”。最好的地方是就在那个函数内,错误发生的那一刻处理掉。

Carlisia Thompson: 如果人们真的对每次写 if 语句来检查错误感到厌烦,他们可以使用你的 errors 包,对吧?我特别喜欢你设计的方式——你只需返回错误,如果是 nil 就是 nil,如果不是,信息就在那里,仅此而已。

Dave Cheney: 是的。



Erik St. Martin: [59:47] 我记得我刚接触 Go 语言时,这种繁琐的错误处理也让我感到有点烦,因为我之前使用的语言都有异常处理机制。然而后来你会意识到,这样做其实更合理。但我认为这其实是一个视角转换的问题。它就像是“半杯水”的问题:你是觉得这是件烦心事,还是觉得这其实是一种保障?我们经常会想,“哇,我得一直这样做,真烦人”,但当你换个角度想,所有与你一起工作的其他人也都得这样做时,这是不是就更让人放心了?这有点像 HOA(房主协会)规定。[笑声] 有点让人觉得烦,对吧?因为你不想遵守这些规则,但它要求其他所有人也必须遵守这些规则。

Brian Ketelsen: 我认为另一个类似的例子是 Go 中接口的概念。今天我在思考这个问题时,觉得 Go 中处理接口的方式与处理错误的方式在某种程度上是相似的。在 Go 中,你是在建模行为,而不需要花太多精力去思考继承链以及要在它们之上创建哪些抽象类。我以前做过很多 Java、C# 和 Ruby 编程,所有这些面向对象的继承确实带来了巨大的认知负担。

在 Go 中使用嵌入和组合就感觉轻松多了,也好多了,但对于那些来自面向对象语言背景的人来说,要感受到这种轻松感是很难的。同样,这也是一个需要时间去适应的新特性,大家在习惯之前都需要一些时间去调整。

Dave Cheney: 我同意,这确实是我演讲时提出的一个悬而未决的问题。在每一期播客中,每次你采访某个人,总会有人在某个时候说:“哦天哪,你应该看看我写的第一段代码……我用了太多的 channels,而且还用错了。”现在每个人都知道这个教训了。任何一个成功的 Go 开发者都会说:“哇,我用了太多的 channels,我过度了。” 但是对一个新的 Go 开发者来说,“不要使用太多的 channels” 这个建议并没有实际操作性。他们会想:“你在说什么?这不就是我学习这门语言的原因吗?显然并发是个非常重要的事情,为什么我不能使用 channels?”

这就变得非常主观,并且对新手开发者没有太大的帮助。说“要小心,不要用太多 channels”并没有提供一个明确的设计语言来告诉他们:“在什么情况下使用 channel 是合适的,什么情况下不合适?” 这些讨论在 Go 社区的整体话语中是缺失的,或者即使不缺失,也没有得到足够的强调。这些并不是我们经常谈论的内容。我们更关注速度、静态编译、交叉编译或其他类似的东西,这些也很好,但它们有点忽视了更大的问题……

Erik 提到过,Ruby 社区非常注重设计。人们总是讨论语言设计,这些是你在会议上会谈论的事情。那种讨论语言设计的氛围在 Go 社区里在哪里?我认为这正是我演讲中提出的悬而未决的问题。

Carlisia Thompson: 我很高兴看到,现在关于这个话题的讨论越来越多,像你、Ben Johnson 和 Mat Ryer 都在推动这个话题。Mat Ryer 还写了一篇博客文章,讲的是他在 Golang UK 会议上的演讲,讨论了“首先检查错误,然后执行正常路径”,就像你刚才说的。这其实就是那个设计概念:“给你的方法加一个保护措施”。

回到你提到的关于设计的讨论,当我刚开始学习 Go 时,我遇到了一些专家,问他们“你是怎么学会的?” 他们的回答总是“通过不断地试错”。现在我看到人们在思考:“我们不能再让别人通过试错来学习了。”

[01:04:14.01] Katrina 在丹佛 GopherCon 的演讲也涉及到了这个概念:“我们要为新人铺好道路,让他们不必经历试错的折磨。我们应该有教育资料……” 现在有很多人开始谈论设计。我感觉到这是一个积极的转变,我认为这是非常好的。

Dave Cheney: 是的,Katrina 的演讲非常精彩,我们在这期播客中应该记住的是,我们是成功的故事。我们是那些没有放弃的人,我们没有迷失在代码的混乱中,我们最终找到了写成功的 Go 代码的方法。Katrina 的演讲非常重要,因为她提到,作为一个初学者,你没有那种……有句俗话说“事后诸葛亮”(Hindsight is 20/20)。我们都在回顾自己的经验,说“哦,学习很难,但我们总算挺过来了。” 但试着把自己放回初学者的心态,你没有判断对错的概念,不知道自己写的是好代码还是坏代码,不知道自己是从其他语言中带来的知识,还是从零开始学习的。你无法判断,自己运行程序时遇到的困难是因为语言本身,还是因为自己犯了错误?

作为初学者,你没有背景来评估自己,评估自己的进度。Katrina 的演讲提醒了我们所有人……我们是成功的故事。我们熬过了试错的阶段,或者恰好在合适的时间读到了正确的示例,走上了成功的道路,但我们不应该认为每个人都能做到这一点,因为很多人半途而废了。

Carlisia Thompson: 是的,说得好。

Erik St. Martin: 我认为我们已经超时了,所以这期我们就不讨论新闻和项目了,尽管我们很想继续聊下去。我们能做一个 12 小时的播客吗?[笑声]

Brian Ketelsen: 如果有 Dave 在,应该可以,哈哈。

Erik St. Martin: 那我们就直接进入 #FreeSoftwareFriday 环节,然后结束节目。Brian,还是由你来开始吧?

Brian Ketelsen: 好的。这周我花了很多时间在 rsync 上。虽然是老派的 UNIX 工具,但我离不开 rsync,所以我要特别感谢那些让 rsync 变得又快又好又安全的人。我爱 rsync,谢谢你们。

Dave Cheney: 我记得好像是 Jeremy Allison 开发的,对吗?做 Samba 的那帮人也做了 rsync。也许我记错了,但这两者之间确实有很强的关联。使用 rsync 和 SSH 基本上可以移动整个世界。

Brian Ketelsen: 对,我这周基本上就是这么干的。[笑声]

Dave Cheney: 轮到我了吗?我也有一个推荐。

Erik St. Martin: 来吧。

Carlisia Thompson: 好的。

Dave Cheney: 几周前有人在播客里提到 AG……

Erik St. Martin: 那是我。

Dave Cheney: 我有一个更好的工具,叫 pt——Platinum Searcher,是一个日本 Gopher 写的,我会把链接放在节目笔记里。Pt 比 Ack 好得多,也比 AG 好……我每天都在用它。我不太喜欢编辑器集成;我用的是非常简洁的环境,所以我的整个工作流程就是 pt,找到一行,编辑它。

Carlisia Thompson: [01:08:06.06] 我非常期待安装它。听你这么推荐……我一直在用 Ack,我也不在编辑器里用它,只在终端里用。听起来这个工具要好得多,我迫不及待想试试。

Dave Cheney: 它有 Ack 和 AG 的所有功能,比如跳过临时文件和 .git 文件夹之类的东西,而且它是用 Go 写的。

Erik St. Martin: 我也在想是谁推荐给我的……好像是 Harald Ringvold,也许是在 GoTime FM 的 Slack 频道里。他就在节目开始前提醒我说:“你见过这个工具吗?” 所以我一定要装一下。你说它的功能基本上和 Silver Searcher 相同?

Dave Cheney: 差不多……其实我不知道它有哪些功能;我只用 pt。我有两个需求,一个是 pt,另一个是 pt-l。Pt-l 给你文件名,你可以通过 Vim 管道编辑文件。它是 Monochromegane 写的,一个日本 Gopher。我在 2014 年第一次参加日本的 GoCon 时看过她的演讲。

Erik St. Martin: 太棒了。而且当我们找到新的用法时,还可以贡献代码。我可能和你有类似的工作流程。我基本上也是用 AG 搜索一个目录,然后在 Vim 里打开文件。我不做太多的编辑器集成,习惯了开着编辑器和终端。

Brian Ketelsen: 我刚安装了这个工具,真是太美妙了。

Erik St. Martin: 你比我们快一步。Carlisia,你呢?

Carlisia Thompson: 我最近刚找到一份全职做 Go 的工作,我在一个新项目上,经历了设计和定义的阶段,最近开始真正进入编码阶段。今天我要推荐的——我们之前提到过——是 Sourcegraph。我无法形容它让我搜索东西的速度有多快。如果我在 GitHub 上,我不想把所有东西都下载到本地机器上再搜索;我可以那样做,但 Sourcegraph 快得多。我在 GitHub 上浏览一个库的代码时,Sourcegraph 会弹出描述。如果我想深入了解,我只需点击链接就可以直接查看。真的非常棒。

它让我学习的速度提高了百倍,不仅能快速找到我需要的东西,还能同时学习 Go 的模式。另一个推荐是 Go 本身——我非常喜欢它。我在别人的库里看到的 Go 代码,和我自己写的 Go 代码几乎是一样的。代码的一致性非常棒,这让生活变得轻松多了。

Erik St. Martin: 太棒了!我这周的推荐有点不同,最近几周没有使用太多新的编程工具,我觉得大家可能也不想一直听我推荐老工具,所以我要推荐 Asciidoctor,网址是 asciidoctor.org。通常我用 markdown,但我还没找到一个能很好地处理目录等功能的工具。Asciidoctor 类似于 markdown,它使用 AsciiDoc,然后可以在后台转换为 DocBook,生成带有可链接目录的 PDF。你可以做一些边栏和注释,也有代码高亮功能,所以它非常适合做带代码的文档。

Brian Ketelsen: [01:11:57.12] 不错。

Dave Cheney: 是的,我经常用它。我所有的演讲稿都是长文形式的;我会把所有想说的话都写下来,然后再转录到 Keynote 或其他工具里。首先,我需要知道我写了多少字,而 Keynote 不会告诉你。我习惯把所有内容都写成长文,然后用 AsciiDoc,这样我就可以转换成 HTML,发链接给别人。

对我来说,这比用 Google Docs 好多了,因为用 Google Docs 你得在线,而 AsciiDoc 只是一个文本文件,你可以随时随地编辑。

Carlisia Thompson: 不错。

Erik St. Martin: 而且我们还能用 Vim。一个简单的 guard 文件,旁边更新 PDF……然后我还要感谢 Afrin,让我在节目中不再抽鼻子。

Carlisia Thompson: [笑] 这是开源的吗?

Brian Ketelsen: 不是开源的,而且你现在可能需要在药房柜台出示身份证才能买到。

Erik St. Martin: 不过成分都列在包装上,所以某种程度上它是开源的,对吧?[笑声] 算不上完全开源,但也差不多了。

Carlisia Thompson: 这又让我想起了 Katrina 的演讲……[笑声] 成分你有了,但你不知道怎么组合起来。

Erik St. Martin: [笑] 那个正确的配比……好吧,虽然我很想继续聊下去,尤其是有 Dave 在,今天我们聊了很多有趣的话题,而且还有很多可以展开的讨论,但我觉得我们已经超时很多了,这会是一期很长的节目。我非常感谢今天所有参与节目的嘉宾,也感谢所有的听众,无论是现在的还是之后收听节目的。特别感谢我们的赞助商 Backtrace。

如果你还没有订阅,可以在 GoTime.FM 上订阅。我们可能会推出一个新闻通讯。我们的推特是 @GoTimeFM,如果你想推荐自己或提议嘉宾或问题,可以通过 GoTime.fm/ping 在 GitHub 上提交。

我想就这些了。感谢大家,再见!

Brian Ketelsen: 感谢你这么早起,Dave。

Dave Cheney: 不客气,谢谢你们邀请我。

Carlisia Thompson: 非常感谢你,David。

文章目录