Go相关工具

本篇内容是根据2019年6月份#90 Go tooling音频录制内容的整理与翻译

这一期谈论我们每天使用的工具来帮助提高工作效率!这对于那些刚接触 Go 工具的人来说是一个很好的介绍,并围绕我们使用其中一些工具多年后对它们的看法进行了一些讨论。


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



Mat Ryer: 大家好,欢迎收听 Go Time。我是 Mat Ryer,今天我们要聊的是工具。那些帮助我们取得成功,帮助我们完成工作的好工具。我们每天都在使用 Go 工具,我们用它们来构建、运行代码、测试,也用它们来格式化我们的代码、进行代码检查和验证,还有很多其他用途。

我认为这期节目对刚接触 Go 的人会很有帮助,可以让他们了解我们使用的这些工具。我也相信那些经验丰富的 gopher 们也会从中获得一些有价值的内容……我之所以这么有信心,是因为今天和我一起的是一些特别优秀的嘉宾。今天和我一起的是---没有特定顺序---Jaana Dogan。你好,Jaana。

Jaana Dogan: 你好,嘿!

Mat Ryer: 欢迎回到 Go Time!你最近怎么样?

Jaana Dogan: 是啊,挺久没来了。我最近一直在旅行吧。

Mat Ryer: 哦,去哪儿了?

Jaana Dogan: 我去了西班牙的马尔贝拉(Marbella)。上次我们聊天的时候我正准备去参加一个会议,然后我就再也没回到节目里了。很抱歉……

Mat Ryer: 没事,我理解。今年你为了工作到处去一些异国风情的地方旅行……真是艰难的生活啊。

Jaana Dogan: 的确是很“艰难”的生活,哈哈。

Mat Ryer: 你之前跟我说你在工作中做的所有事情都是绝对保密的。你要不要就破个例,告诉我们一些呢,或者……

Jaana Dogan: 嗯,其实嘛……我正准备换一个新岗位。我是说,不是新工作,但算是一种新的角色。目前我还在探索我到底应该做什么……保密并不是因为它真的很机密,而是我自己也不确定我会专注于什么,所以……我觉得我还需要一周左右的时间。别太紧张,这不是针对你,只是……我还在摸索中。

Mat Ryer: 当然,虽然我确实有点伤感,但我会假装不在乎的(笑)。另外,今天的节目还有 Johnny Boursiquot。你好,Johnny!

Johnny Boursiquot: 大家好。

Mat Ryer: 说到新工作,你刚刚开始了新的工作对吧?

Johnny Boursiquot: 是的,最近才开始,才几周时间。我还在所谓的“入职阶段”。不过很兴奋,我很期待能贡献和学习……新工作总是这样让人兴奋。有个蜜月期,期间一切都是新的,你在学习,了解系统和团队……但某一刻你会跨过那个门槛,感到“哎呀,这到底怎么回事……我得开始修复一些东西了。”不过到目前为止一切都很好。

Mat Ryer: 哦,真高兴听到这些消息。确实很让人激动,虽然也有点可怕,新工作总是这样。但我祝你一切顺利。如果你不介意的话,我们在节目中可能会继续问你有关新工作的事,因为我很感兴趣,我觉得这对其他人也有帮助,能听听我们在职业生生涯中都遇到什么……所以如果你不介意的话,我会继续打扰你。

Johnny Boursiquot: 当然,没问题。

Mat Ryer: 那我们直接开始吧。今天我们要讨论 Go 工具。我之前在 Twitter 上问大家,哪些 Go 工具是他们最喜欢的,或者哪些工具他们觉得最好用……我先来吧,我最喜欢的应该是 go fmt(go format)。对于那些不太了解的人来说,它是用来格式化所有 Go 代码的工具,让代码看起来都一样。而所有的规则都已经内置在这个工具里了。你不能选择用空格还是制表符,不能选择大括号放在哪儿,实际上你没什么可以选择的余地……

对于一些习惯了能够配置所有这些细节的工具的人来说,他们可能会觉得这是 Go 的一大缺陷……但在我看来,这反而是 Go 的一个超级优势。因为这意味着所有 Go 代码都开始看起来一样,变得很熟悉……我曾经在一个项目中发现代码看起来像是我写的,但实际上我绝对没写过。这真的很棒。

如果你想想代码审查,特别是涉及空白的情况……有时候 pull request 里有很多空白内容,导致很难看出实际的变更点。而使用 go fmt 我们就没有这个问题,因为它会把所有东西都格式化得很好。

还有其他人吗?你们对 go fmt 有什么感觉?你们怎么发音的?我们先把这个问题解决吧。

Jaana Dogan: 是 “go fmt”,对吧?

Mat Ryer: 好的,没错。

Jaana Dogan: 至少我是这么叫的(笑)。

Mat Ryer: 我也是这么听说的。

Johnny Boursiquot: 其实我在教学时提到 “fmt package” 的时候总觉得有点别扭……别人会有点怪怪地看着我,我就会说“是的,我知道,别在意。”如果你说 “fmt” 或者 “format”,天哪,gopher 们可能会有点不习惯,但我们就是这么叫的。

Jaana Dogan: 是的,一开始人们确实需要时间去适应,但一旦学会了,他们就接受了,也不会再质疑了……我尽量保持一致,叫它 “go fmt”。

Mat Ryer: 是的,我也是。我不知道这是不是自然而然的,但我听说了之后就开始这么叫了,为了保持一致。

有趣的是,有时候人们会说 “golang”,因为我们在 Google 搜索或者使用标签时会写 “golang”,但我们从不说 “golang”。所以这是给 Go 新手的一个小提示---当你谈论这门语言时,只叫它 Go,不要说 “golang”。fmt 也是一样的道理。

Johnny Boursiquot: 说到 go fmt,让我稍微回顾一下。当我第一次接触到 go fmt 时,老实说,我有点吃惊。因为我之前没习惯这样的工具来格式化我的代码,使其看起来统一。我来自的编程语言社区里,每个人都有自己的一些小偏好,比如“我喜欢大括号对齐”,另一个人可能会说“我喜欢大括号和声明在同一行”,等等。

人们会就这些风格问题争论不休,讨论什么更易读,什么不太易读……而且显然这些都是主观的。每个人都有自己的偏好,自己的习惯和不习惯的东西……而 go fmt 把这些全都抛开了。

老实说,在最初的一个月里,我其实不太喜欢它做的所有事情。我对其中 90% 感到满意,但有些地方我并不喜欢。但随着时间的推移,我越来越喜欢这个工具以及它所做的事情。它的好处---我觉得你也提到了,就是所有的 Go 代码开始看起来像我期望的那样了。那种认知负担---审查代码时的负担---就完全消失了。我不需要再担心“这个人的代码格式会不会和另一个人不一样?”我可以专注于代码的实际功能,而不是去解析“这个人的风格是这样,另一个人的风格是那样”这种事情。所以它在这方面非常有价值。

Jaana Dogan: 是的。Robert Griesemer 曾经说过一句话,他是负责维护 go fmt 和其他工具的人。他说他并不完全同意所有的代码风格;他不一定完全认同 go fmt 的所有规则,但有工具来强制执行,这样就没有争议了。

我在一家非常大的公司工作,我看到过---仅仅为了修改 Java 风格规范中的一个小细节,竟然花了四年时间。而且你能想象吗?有成百上千的人对风格有着强烈的意见,四年时间就浪费在这种微小的风格问题上……我很喜欢有 go fmt 这样一个规范的工具,没有争论,只有一个真理的源头,所有人都必须遵守它,即使格式并不总是你想要的那样。

Mat Ryer: 对。如果这个工具不是最开始就有的,而是现在才出来的,你觉得大家还能像现在这样团结在它周围吗?还是说这得益于它自一开始就存在?

Jaana Dogan: 我觉得一开始就创建一些文化是很有必要的---比如依赖一个工具……因为这会在社区里形成一种共识,让足够多的人理解为什么它有价值。如果你在后期才引入这样的工具,社区已经分裂了,大家都有理由偏向于个人的风格,因为他们已经(比如)在整个公司范围内投资于某种特定的风格,后期很难再去修正这些问题。所以我觉得他们一开始就推出这个工具是非常好的……至少这是我的看法。

Mat Ryer: 是的,我同意你说的。有几个例子可以说明最初团队的远见或洞察力---我们现在真的从这些决策中受益了,后面我们也会讨论更多类似的例子。我觉得另外一个例子就是 go test 工具---它也是从一开始就有的。所以测试作为一个概念在 Go 中是一级优先的。这当然是有原因的,因为 Go 设计的时间点正是我们通过写测试来构建软件的时代。测试在软件工程中非常重要……但他们在早期就做出了这些决定,确立了一个先例……从那时起,这些决定每天都在带来好处。

Jaana Dogan: 是的,我觉得 Go 在识别软件工程中最重要的 80% 的内容方面做得很好,而工具也反映了这些优先级。

Mat Ryer: 好吧,既然我们已经谈到了 go fmt,如果我们再看看 golint 和 govet,有没有人想尝试解释一下这两者之间的区别,或者描述一下它们到底做什么?[停顿] 好吧……[笑声] 是的,golint---我喜欢它。它本质上会查看你的代码,进行一些静态分析,并能捕捉常见的错误,并给你一些警告。有时候它们不是错误,而只是一些最佳实践。你可以在你的代码上运行这些 lint 工具,看看它有没有建议你可以改进的地方。

举个例子,如果你在包里有一个导出的东西,它的名字是以大写字母开头的,那么你应该给它加上一个注释。这是公认的做法。Go 规范并没有规定这一点,所以如果你没有加注释,它不会是编译错误……但 golint 工具会捕捉到它,并告诉你 “为了达到最高质量,你应该考虑在这里加上注释。”

而且关于如何编写注释也有一些规则,比如我们会在注释的第一句话中重复名字。所以有很多类似的细节被编码到这个 linter 里,对吧?

Jaana Dogan: 是的。我想我们首先需要提到的是,lint 和 vet 是不同的。vet 是报告一些可疑的东西,以及一些可能会误用 API 的模式,可能会破坏内存,或其他问题。典型的例子是 printf---如果你传递了错误类型的参数,它会抱怨。

所以 lint 更多的是关于风格错误。例如,godoc 中的公共 API 会抱怨类似类型的问题。这成为了测试的一部分,但并不是 govet 报告的所有东西都是真实的。据我所知,可能会有误报。这同样适用于 lint。这些工具不是编译器的一部分,因为有些报告可能不准确,但它们通常生成足够真实的报告,而且它们非常有用。

Mat Ryer: 是的,你说得对。如果你使用 printf 或 wrapf,使用这些 f 方法但没有放入正确数量的动词/参数,捕捉到这种问题非常有用,因为一眼看出这些错误是相当困难的。所以我认为人们应该在他们的代码库中启用这些工具,至少运行它们,看看它们实际在说些什么……因为你可能会发现你同意它们的建议。

注释问题是一个很好的例子。它相当教条主义,只是简单地说 “好吧,它是导出的,所以需要注释。” 现在,如果函数叫 “new thing”,那么显然它是在创建一个新东西,而你的注释可能会说 “new thing makes a new thing”(新建东西创建新东西)。所以我们有一点冗余,但我认为总体而言,如果你遵循 lint 工具的建议,我发现代码会看起来更熟悉,你还会获得 go fmt 的所有好处。

Johnny Boursiquot: 我通常做的一件事,以及为什么我头脑中没有立即区分 lint 和 vet 的原因之一是,我从未真正深入讨论它们的区别,因为它们已经成为我工具链的一部分。在我的日常工作中,我使用很多 VS Code 和 Vim 作为我的编辑器,它们有内置的插件和扩展;这已成为我工作流程的一部分。所以每次我按下保存键时,这些工具就会运行,我会在不同的地方看到来自不同工具的标记。

还有另一个流行的开源项目,我想叫做 gometalinter,它也包含了很多这样的工具。你可以配置它,关闭一些工具,打开另一些工具。这些工具一起给你一组输出,你可以逐一查看,发现 “哦,是的,我在这里用错了动词。我应该使用整数,但我用了字符串。” 如果你单独运行这些工具,linter 和 vet 会为你找到这些问题;但因为它们是我工具链的一部分,我基本上只需要看看编辑器底部的视图,得到一堆需要修复的东西。

我几乎不在意是哪一个工具给了我什么,除非我真的需要与一个特定工具打交道……但它们是我编辑器的一部分,每次我按保存键时,代码格式化就会完成,goimports 也会自动处理。所有这些事情都在发生。工具让你可以专注于写代码,而不必担心每次都单独运行工具。

Jaana Dogan: 是的,实际上将它们作为整体体验的一部分真的很有用。尤其是 vet,它报告了很多有用的东西,比如 “嘿,这段代码不可达”,或者你传递了一个非指针给 unmarshal 之类的……有时候你在编写代码时,发现这些问题真的很难,但工具在帮助你在编程时做正确的事。

Mat Ryer: 是的,我也把它扩展到运行测试。我倾向于编写单元测试,它们运行得非常快,然后你可以在每次保存包时运行它们。如果它们变慢了,你当然需要有不同的策略,但至少在开始阶段,如果是单元测试,它们运行得非常快……而且 Go 的构建速度仍然很棒。我们总是忘记这一点,直到你不得不去构建另一个代码库;然后你又会感激它。

顺便说一下,Johnny,现在 gometalinter 改名为 golangci-lint 了。所以如果你想在 VS Code 中安装它,现在就叫 golangci-lint

Johnny Boursiquot: 有意思。

Mat Ryer: 是的,你说得对,它是一个 metalinter---它运行一系列其他 linters,并给你一个全面的视图。它们也与 IDEs 完美集成。就像你说的,你可以在保存时运行它,但即使你不这样做,你通常也可以将它集成到 IDE 中,使其成为你日常工作的一部分……因为你知道,每次你从代码中得到实时反馈,这都是有价值的,因为通常当你在工作时,你也在学习。这是一个很好的学习方式,因为当你编写代码时,linter 会告诉你 “这段代码现在不可达” 或者 “那部分代码现在在那边。” 如果还有测试,那么 “哦,这些你没想到的测试在那边失败了。” 你从代码中得到的反馈在工作时非常有用。

Johnny Boursiquot: 你不应该等到……如果你有持续集成(CI),你应该这么做;但你不应该等到代码到达远程服务器,所有这些工具都运行时,才得到反馈。当它成为你工具的一部分时,反馈循环快得多,就像你说的那样。所以你可以在本地做一些事情,确保你的代码已经过 fmtvetlint 等等,然后才提交代码。

然后当代码进入审查或 PR 阶段时,无论你使用什么 CI 工具---Travis、Circle,还是其他现在数不胜数的 CI 工具---它们会给代码一个认可。这样一来,人们可以只关注代码做了什么,而不用告诉你 “嘿,你忘了运行 go fmt。” 这些工具非常好,我完全鼓励大家将它们作为开发工作流程的一部分。

Jaana Dogan: 是的,最棒的一部分是它们真的很快。它们是编辑体验的一部分,因为它们快。我来自一个使用大量 Java 工具的背景,体验并不像现在这么流畅。我们过去也有类似的静态工具,但体验不如这些 Go 工具流畅。没有人会因为它们影响编辑体验而选择不使用它们,因为它们既快又有用。

Mat Ryer: 我们提到了 go test。这是我们经常使用的另一个工具。对于那些没有用过它的人---如果你在 Go 程序中编写测试代码,你通常通过在文件名末尾加上 _test.go 来命名文件,然后你运行 go test。它会查找所有这些测试文件,并真正运行所有的测试代码。如果你做了 TDD(测试驱动开发),你就知道你的代码实现了它的承诺,做了你说它要做的事情。

测试工具中还有一个小功能,我觉得常常被忽略了……那就是竞态检测器(race detector)。当你编写并发代码时,你可能会打破规则,尝试同时从同一数据中读取和写入;如果你尝试这样做,这是非法的,程序会崩溃。但当然,如果你已经写了并发代码,这种问题有时很难发现,尤其是很难为它编写测试,因为有时候问题可能不会发生,因为调度的方式……但 go test 有一个 -race 标志,虽然运行速度会慢一些,但它会做一些额外的检查,并且可以在早期捕捉到这些潜在的死锁……这非常酷。

Jaana Dogan: 是的,工具是标准工具的一部分。这不仅仅是测试,它是一个很好的补充功能,像竞态检测器也是测试的一部分。

Mat Ryer: 嗯。不过这会增加额外的负担,对吧?而且它会减慢你的程序运行速度,不是你总是会开启的东西。

Jaana Dogan: 是的,这就是为什么我认为将它作为测试的可选功能是有用的。但除此之外,你不希望始终开启竞争检测器。

Mat Ryer: 是的。

Johnny Boursiquot: 我的经验是混合的,显然这取决于代码库的大小……但最近我一直在处理一些小型代码库。我一直在做微服务相关的工作,这类代码库相对较小……所以默认情况下,我使用 make 命令,并且默认情况下我运行带有 -race 标志的测试。

我没有注意到明显的性能下降,但显然,这可能因项目的大小和你正在处理的其他内容而异。

Jaana Dogan: 之前有一个关于这方面的基准测试……我记得如果你开启了竞争检测器,内存使用量大概会增加五倍。而且我记得在执行时间方面---同样,有一些报告,但这真的取决于具体的使用场景……它确实会增加一些额外的负担,可能是 2 到 20 倍左右的开销,如果我没记错数据的话。实际上在 golang.org 上有一篇非常好的博文或文章,介绍了竞争检测器,那里面应该有一些具体的数据。

Mat Ryer: 嗯,太好了。我也在想 go getgo get 是另一个工具,我觉得---显然,尤其是在模块领域,很多东西已经发生了变化……但不得不说,当我第一次使用 Go 时,只需要执行 go get 加上包名就能安装包。而且那个包名既是导入路径,也是包所在的 URL,我觉得这是一个非常优雅的设计,安装东西变得非常简单。在 GOPATH 的世界里,所有东西都被放在一个地方,但 go get 让一切都变得非常容易。你觉得 go get 和新的模块工具相比怎么样?因为使用模块确实要复杂一些。

Johnny Boursiquot: 模块部分我留给 JBD 来处理,但我可以告诉你,当我使用 go get 时,尤其是在教学时,能够说“看,我们要导入这个包。在我们真正导入这个包并在代码中使用它之前,我们需要使用 go get。”所以我实际上会说“好,go get”,然后我会找到 github.com 上的包名,或者任何公共仓库的包名……然后我会看到学生们一脸茫然,他们会说“好吧,刚刚发生了什么?”

然后我意识到,如果我直接复制那个路径,打开浏览器,粘贴到 URL 栏并导航到那个仓库,他们立刻就明白了:“哦,明白了。你实际上是在从这个路径拉取代码;你在命令行上拉取它。”现在他们可以在浏览器中看到并阅读那些代码,了解他们实际上拉取了什么。因此,关于拉取包并放入 GOPATH 的所有概念对他们来说毫无意义,但一旦他们能在浏览器中查看那个路径,一切就豁然开朗了。他们理解了 go get 的价值,并且不再关心它被放在哪个 GOPATH 目录中。知道如何获取它,并知道去哪里查看拉取的内容,对他们来说简直像是魔法。

Mat Ryer: 有趣的是,这并不是魔法,而是非常显而易见的东西,即“这是 URL。去看看吧。你知道 URL 是什么。”我觉得这很棒,而且你刚刚讲述的小故事完全说得通。如果我为一个项目使用一些 npm 的东西,我安装了几个包,然后我看看 Node 模块的文件夹---里面有 1600 万个文件夹。[笑声] 而我根本不知道它们是从哪里来的……它有点隐藏了。这确实像是魔法。而通过这种简单且清晰的方式,即使你为此牺牲了一些功能,我认为这种正面的回报是巨大的,并且会在以后不断带来好处。

Jaana Dogan: 我觉得我们应该专门做一期关于 Go 模块的节目,但我同意 go get 确实提供了一个非常好的初始体验。我喜欢的一点是---如果你使用 go get 获取一个主包,它会安装这个包,并将其放在你的 GOPATH 的 /bin 目录中……所以这也是一个分发工具的好方法。在使用 Go 之前,我只是发布二进制文件,并确保我在所有地方都使用正确的版本。版本控制在 go get 中依然是个问题,但我认为这是一个可以接受的妥协。

Mat Ryer: 是的。我接下来要做的就是继续讨论其他 Go 工具,因为我自己也从中学到了很多东西……另一个我喜欢的 go build 的功能是我们可以进行交叉编译。这个功能从一开始就存在了,我认为。

Jaana Dogan: 是的。

Mat Ryer: 基本上,对于那些不知道的人,你可以选择目标架构和目标机器来为其构建你的 Go 代码。如果你使用 Docker,这非常有用,因为你可以在 Mac 上为 Docker 构建,然后你会得到一个 Linux 的二进制文件,你可以将其放入 Docker 镜像中。当然,你也可以将代码放入 Docker 并在那里,在那个环境中进行构建。你在交叉编译方面的经验如何?

Jaana Dogan: 我觉得这简直是魔法。当我第一次看到他们输入 GOOS(发音为 goose),然后输入 Windows 并执行 go build,你就会得到一个 Windows 的二进制文件……这感觉像是“哇!”这太神奇了。我通常是为 Linux 生成二进制文件,所以我可以在 Mac 上无忧无虑地工作。这真的太赞了。

Mat Ryer: 是的。Johnny,你用过吗?

Johnny Boursiquot: 当然。我第一份全职使用 Go 的工作,我的首要任务之一就是创建一个多平台的构建流程。我非常依赖 GOOS 和 GOARCH。对于那些不知道 GOARCH 的人---这是 GOOS 的搭档,表示 Go 的架构。使用 GOOS 和 GOARCH 是完成这项工作的关键,能够为各种不同的平台生成二进制文件。

Go 支持的架构组合非常多,ARM 处理器……你可以生成的不同组合实在是太多了,我已经数不过来了。这个功能真的是一个福音。如果没有这些功能,我不可能完成那份工作。

Jaana Dogan: 我觉得这也很棒……我之前为 ARM,特别是 Raspberry Pi 做了很多开发……Raspberry Pi 上的处理器和我的笔记本电脑是没法比的,所以我会在我的笔记本电脑上构建,因为速度会更快,然后我把它推送到 Raspberry Pi 上,因为交叉编译实在是太容易了。速度可能快了十倍左右。

Mat Ryer: 哇。那这到底是如何工作的呢?因为显然,编译器会执行几个步骤,最终生成由机器码组成的二进制文件。是不是只是机器码根据架构的不同而生成不同的指令?

Jaana Dogan: 是的,他们知道每个架构需要生成什么,所以他们基本上会获取输入,知道如何映射它,然后根据操作系统和架构生成输出。

Mat Ryer: 这一定是因为他们构建工具系统的方式才实现的。你觉得这是他们有意为之的,想要能够为任何目标架构生成代码,还是他们在构建之后才意识到他们能够做到,因为他们从一开始就设计得很简单?

Johnny Boursiquot: 我不认为你会偶然发现这样的东西。如果让我猜的话,我会说这是有意设计的……考虑到语言的创造者们---基本上他们是为 Google 构建的,所以我想在某些时候他们需要能够在不同的平台上运行二进制文件,针对不同的 CPU 架构;比如 32 位和 64 位。所以我认为这一定是计划的一部分,设计的一部分。这看起来太复杂、太强大了,不可能是从构建系统中意外衍生出来的。

Jaana Dogan: 我们简化了这个过程,但实际上这中间有一个中间汇编……编译器首先将所有内容翻译成中间汇编语言,然后从那时起再将其编译成特定架构的指令。所以编译器的内部实际上是一个两步走的过程……这也是编译器常见的工作方式。它们首先将所有内容转换为一种中间语言,然后从那种中间语言你就可以针对任何你想要的架构生成代码了。

Mat Ryer: 当然,你也可以使用构建标签。有人愿意解释一下构建标签吗?

Jaana Dogan: 是的,构建标签提供了条件编译的功能,你可以创建不同的规则。例如,你可以设置约束条件,指定“仅在构建 Linux 时使用这个文件。”或者你可以说“我只希望这个文件在 ARM 架构的构建中被包含在内。”工具链提供了许多不同的规则。GOVERSION 是其中之一,任意的自定义构建标签也是其中之一……因此它为你提供了根据 Go 版本、操作系统或架构、或一些自定义构建标签切换不同实现的可能性。

Mat Ryer: 是的,我在测试时成功使用过这些标签。有时,如果有一些长时间运行的测试,或者有需要不同依赖项运行的集成测试,我会在测试文件中使用构建标签。这是选择要运行的测试子集的一种简单方法。这只是文件顶部的一个特殊注释,对吧?

Jaana Dogan: 是的,我觉得它是在文件的顶部,有一个特定的位置……就是这样。而且它非常易读。我唯一的抱怨是,关于这些规则,关于构建约束,有时要表示多个规则真的很难。这会变得非常难以解析。如果你想要有更复杂的规则,比如“嘿,在 Linux、Darwin 上包含这个文件,但不在这个特定的环境中;此外,不适用于这个自定义构建类型。”我觉得表达这些更复杂的约束有点困难……但除此之外,我认为它非常直接,我一直在使用构建标签。

Mat Ryer: 好的,我还想提到一些来自社区的工具。记住,我们一直在使用 Go 工具,但我们也可以编写工具,一些人已经贡献了工具。我认为 goimports 是 Brad Fitzpatrick 的项目;这是他的一个想法,他自己完成的……它基本上封装了 go fmt,所以你可以获得所有的格式化功能,但它还会为你解决导入的问题。你也可以使用你自己的工具完成这些事情。

有些工具也不一定是我们本地运行的 Go 工具。Matt Holt 有一个出色的 JSON-to-Go 服务。如果你在 Google 上搜索“JSON to Go”,你可以将一个 JSON 数据粘贴进去,然后它会为该 JSON 数据生成相应的 Go 结构。这非常有用,尤其是在你需要消费一个 API 并需要所有数据,而你又不想坐在那里一个个手动输入所有字段名时。所以这是一个非常有用的工具,它是一个托管网站,你可以直接访问。

还有其他我们喜欢的社区工具吗?

Johnny Boursiquot: 我个人喜欢 Go Report Card,虽然这不是一个本地工具,但它可以评估你的代码与 Go 社区约定的惯例有多接近。我认为它可能还集成了一些我们之前提到的工具---如 linter、vet 等工具……它还包括一些其他的东西,比如圈复杂度分析,还有其他一些不错的功能。基于这些工具,它会为你的代码库打分,评分范围是 A 到 F 级别。

我发现这特别有用,尤其是在评估一个第三方包的时候,决定是否要使用它。如果它有一个评分,我会看一下。如果评分不是 A,我会仔细看看;我会更加犹豫是否将其引入,因为我会想“好吧,你没有遵循哪些最佳实践或惯例?”所以我会检查一下。

有时我可能只是查看一下发生了什么,可能会在本地复制它,而不必引入那个包,如果我不喜欢它的评分。所以这算是另一个数据点,用来帮助你评估代码库的质量。但是的,这是我喜欢看到的东西之一。

Mat Ryer: go doc 也是一样。go doc 是一个你可以在本地运行的工具,但我们也有 godoc.org 这个托管服务,它让我们可以查看任何开源项目的文档。我觉得这也很不错。这是一种很好的提供此功能的方式,因为它很有意义;你想要分享的只是一个链接。

GoDoc 的好处是---它的格式是 godoc.org/pkg/importpath。所以你仍然是引用导入路径,我们可以看到它。

Jaana Dogan: 我个人使用很多 Dominik Honnef 开发的工具。他有一个 go-tools 仓库,其中的 Staticcheck 工具包含了很多风格检查和 linting 功能,这是 golint 不支持的。有时会有一些有争议的风格话题,无法合并到官方工具中,所以人们就会把它放到 Go Static 工具里。因此,这是一个非常有用的工具,值得一看。在静态工具方面,我更依赖 Staticcheck 而不是 Golint。

Mat Ryer: 是的。Fatih Arslan (译者注: vim-go的作者) 开发了一个服务,我记得它叫 Fixmie。这是一个 GitHub 集成工具,类似于 Go Report Card,但它实际上会创建带有更改的 PR,就像团队里多了一个成员,一个非常注重风格规则的成员一样。这是一个值得关注的项目。可以去看看 Fixmie。它和 Go Report Card 的想法相似,但与 GitHub 集成得更紧密。

(译者注: 更多可参考 Maintainer spotlight: Fatih Arslan )

有人在这里写过工具吗?无论是静态分析工具还是其他类型的?

Jaana Dogan: 我只写了一些工具,用来从接口生成一些东西……嗯,这些也是一些静态工具……一个常见的案例是生成接口的实现,手动写这些代码往往有很多样板代码,所以我写了一个工具,它会从接口中生成具体的实现,然后你只需要去填充方法的实现。

Mat Ryer: 你用到了 AST(抽象语法树)解析器之类的东西来构建这个工具吗?

Jaana Dogan: 是的,我使用了标准库中的内容。虽然代码不算好看,但能在 100 行左右的代码中完成。

Mat Ryer: 很好,听起来很棒。我们应该花些时间讨论一下我们免费获得的一些性能工具。YouTube 上有很多精彩的演讲,这个话题非常有趣,也有很多不同的角度。Jaana,或许你可以告诉我们一些关于性能工具的事情?

Jaana Dogan: 可能吧,因为我在 Go 项目上工作时,曾参与一些动态工具的开发,这是我全职工作的一部分……我已经在这个领域工作了一段时间了……所以你可能看到过我做的演讲,但我真的不记得了,因为我现在做得太多了。 [笑]

Mat Ryer: 我还以为你做的工作都是保密的呢。

Jaana Dogan: 保密的东西和这个不一样。

Mat Ryer: 哦,那是什么?

Jaana Dogan: 不是我的性能工具,更多的是关于计算产品的……

Mat Ryer: 好吧。

Jaana Dogan: 我们几周后就会知道了。 [笑声]

Mat Ryer: 是的,我只是想模仿那些咄咄逼人的记者,试图挖出你不想说的信息。 [笑声] 但我太礼貌了,你只要说“我不打算谈这个”,我就会说“哦,好吧,再见……”

Jaana Dogan: 问题是我真的不知道。我大概知道我要做什么,但具体的我不确定,而且我是个非常严谨的人。我不想给人错误的印象,误以为我要做一些我实际上不会做的事情,因为那样会让人失望。

Mat Ryer: 完全可以理解。

Jaana Dogan: 开个玩笑啦,哈哈。

Mat Ryer: Jaana,你能不能为不太了解这些工具的人介绍一下它们的作用?

Jaana Dogan: 当然。一般来说,除了性能工具之外,Go 还有很多动态工具,它们是标准工具的一部分。有些工具与性能相关,有些更多是用于调试的……

比如说,性能工具……Go 刚推出时就带有一些动态工具,因为我们去找了 SRE(站点可靠性工程)团队,他们对生产环境中的要求非常具体。他们希望在投产时能有足够的可见性,其中一些需求与性能相关。

他们希望能够获取性能配置文件,获取一些运行时跟踪信息……因为他们特别希望能够理解问题出现时发生了什么,并能够精准定位问题。所以 pprof 支持从 Go 推出早期就内置了。例如,它为你提供 CPU 配置文件、内存配置文件、goroutines、线程配置文件和互斥锁争用配置文件。拥有一个足够成熟的语言来投产是非常重要的,因为大多数人认为性能问题只出现在开发时,但实际上在生产时性能同样重要。

除了 pprof 支持,go test 中还内置了良好的基准测试支持。所以基准测试在 Go 中是头等大事,这在其他语言中并不常见。我认为这有助于形成一种文化,大家都很在意基准测试。我不知道你们怎么看,但我见过很多不同的社区对基准测试有不同的看法,这往往取决于工具的可用性,或者编写基准测试是否容易。你们怎么看?

Mat Ryer: 我看过一些完美的使用案例,也看过一些错误的使用案例。我见过一个例子,因为基准测试写得有点问题,导致报告的结果完全错误。但如果正确使用……这取决于你在测试什么。如果你要测试的是 HTTP 请求之类的东西,由于 HTTP 本身的变化性太大,你不会得到有意义的信息。但如果你有两个小算法,想知道哪个在特定任务上表现更好,那基准测试就非常有用了。

我同意 Jaana 的看法,我喜欢它直接集成在语言中,你只需要写一个以 “func benchmark name” 开头的函数,接受一个特殊变量,然后确保 for 循环的位置正确,考虑设置和拆卸的工作,基准测试就能很好地告诉你哪个更好……有时结果会非常令人惊讶。实际上,我认为这可以成为一个很棒的演讲或展示项目,如果有人想做,可以展示一些代码,然后让大家猜测哪一个运行得更快。我发现有时候结果非常出乎意料。

Jaana Dogan: 是的,我认为基准测试本身是一门学科,需要很长时间才能学会。很多因素都会影响性能。我同意你的看法,我也见过很多错误的基准测试,有些人非常坚定地认为这是优化,但实际上它只在某个特定情况下提高了性能……我认为要写出好的基准测试并正确解读结果,你需要对运行时和语言周围的一切有非常深入的理解。这确实是一件很难的事情。

Johnny Boursiquot: 关于基准测试和性能优化,我尽量不一开始就跳进去。我会先解决问题,然后再进行优化……也就是避免过早优化。而这些工具是标准工具链的一部分,它们使得你可以立即开始使用它们,直接利用它们。

曾经有一段时间---也许现在仍然如此---似乎每隔几周就会有一个新的 HTTP Muxer 或路由器出现,大家都在说“哦,基准测试。相比其他工具,这个没有任何分配,并且比其他工具快 0.05%。”我觉得有点滑稽,因为所有这些都在发生,我觉得我们有点偏离了重点。但拥有这些工具确实很棒,而且像你一样,我还没有见过从一开始就内置这种功能的语言。

我对基准测试非常谨慎,因为它很容易在工程团队内部形成一种文化,即“如果我不能让这个东西超级优化,我就不能发布它”。这有点本末倒置了,所以我倾向于避免这种情况。需要时我才会引入这些工具。

Jaana Dogan: 我完全同意。我认为开发时的优化有点像是人为制造的问题。你会在生产环境中意识到需要优化什么。比如说,我们做的是持续性能分析,我们会从生产二进制文件中不断收集一些配置文件,然后对这些配置文件进行分析,了解哪些地方是热点,哪些在关键路径上调用得更多。如果我优化这个函数会发生什么?从整个系统的角度来看,这个函数的实际成本是什么?

我认为在生产环境中考虑这些情况更有意义,你可以通过观察数据回到开发环境中,然后尝试优化这些内容,并继续使用这些工具。

Go 的 pprof 进行分析的一个优点是,它的开销非常低,你可以在生产环境中启用它。你可以在不严重影响关键路径的情况下持续从生产环境中获取配置文件。当然,它有一定的开销,但有一些策略---如果你有多个 Web 服务器副本,你可以只在一个副本上启用几分钟……根据你将会遇到的延迟,通常是可行的。这就是我们使用的方式。我们会根据使用情况优化系统,找出一些热点路径。在进行任何优化之前,识别这些热点路径也是非常重要的。

Johnny Boursiquot: 没错,先找到问题再去解决。

Jaana Dogan: 是的。

Mat Ryer: Jaana,当你说你进行持续性能分析时,你是说你在部署服务时已经启用了 pprof,然后你再选择什么时候开启它?

Jaana Dogan: 是的,所有的 pprof 工具……pprof 是可以调节的。你可以动态开启或关闭它。所以我们做的就是在几分钟内开启,收集数据,分析然后存储,再将所有数据聚合起来,我们会有每日、每周等的报告……你可以查看“哦,这个服务,特别是这个处理程序,通常被使用得最多,并且这些特定的函数占用了最多的 CPU 时间、内存或其他资源”,然后你可以深入挖掘并优化这些特定的地方。

我希望 Go 能有一些工具来支持这种更持续的性能分析功能。目前写一个工具来聚合多个 pprof 是可行的。可以写一个库,它会定期自动上传报告到某个中心服务,然后关闭等等。我认为我们在这个领域可以做得更好。目前用户需要自己去计划和设计这种事情……但这就是我们所做的。我在这个领域写过一些东西。

有些公司了解这些方法,有些公司不了解。如果社区能够推出更多的最佳实践以及更多的工具,那就太好了。

Mat Ryer: 好吧,这就是号召。如果有人正在寻找一个新的开源项目或想要黑客攻克的东西---这是一个很棒的问题。你能否构建一个工具,定期对运行中的 Go 代码进行采样,并收集结果?这将是非常有用的,可能也会非常有趣。

Jaana Dogan: 是的。一旦你开始看到,例如一个大公司聚合所有的性能分析数据,这就变得很有趣了。例如,你实际上可以通过优化某些函数来减少云服务提供商的账单。你可以说“很多调用依赖于这个函数,如果你对它进行优化,我们的账单可以减少 10%。”一旦你开始系统性地到处执行这些操作,这些优化就会变得非常有用。

Mat Ryer: 我非常喜欢这个信息,即“等到你有了一个正在运行的系统,再去优化它”。有时你可以跳过一些步骤,但总的来说,这个建议非常有道理。能够对运行中的生产系统进行性能分析,以便更好地理解它们,我认为这是一个很好的目标……我们生态系统中的工具非常适合这个用途。

好了,最后爆个料---我觉得我们已经到了这一集的最后。非常感谢 Johnny 和 Jaana。你们觉得怎么样?

Jaana Dogan: 我可以谈论这个话题好几个小时,我觉得这一集非常棒……不过我认为我们应该继续讨论工具。 [笑]

Mat Ryer: 是的,绝对没错。还有很多可以讨论的内容,我甚至可能看看能不能邀请一些社区里开发了我们今天使用的工具的人来加入讨论。

最后再透露一个有趣的小信息---我个人对 Go 项目的唯一贡献就是从 Golint 中移除了某个东西。有一次,感谢我的贡献,Golint 变得更容易通过了…… [笑声] 不用谢。

Jaana Dogan: 耶! [鼓掌]

Mat Ryer: 我喜欢删代码。好的,那就这样了。再次感谢大家,我们下次见,Go Time!

上一篇:构建高效作业管理平台:Spring Boot师生协作评审系统


下一篇:Dockerfile最佳实践:如何创建高效的容器