本篇内容是根据2016年9月份Go modules and the Athens project音频录制内容的整理与翻译,
小组成员 Mat Ryer 和 Carmen Andoh 以及客座小组成员 Marwan Sulaiman 和 Aaron Schlesinger 一起讨论 Go 模块和 Athens 项目。
过程中为符合中文惯用表达有适当删改, 版权归原作者所有.
Mat Ryer:大家好,欢迎来到Go Time。我是Mat Ryer,今天我们要讨论Go模块和Athens项目。今天和我一起的是独一无二的Carmen Andoh。Carmen,您好……
Carmen Andoh:您好,您好!
Mat Ryer:最近怎么样?
Carmen Andoh:我很好。你呢?
Mat Ryer:还不错,谢谢。你猜怎么着,今天我还有另外两位朋友加入我们。你想认识他们吗?
Carmen Andoh:当然,太激动了!
Mat Ryer:想象一下你如果说“不”……
Carmen Andoh:哦,那样的话就太搞笑了---
Mat Ryer:你会把场面搞砸的。
Carmen Andoh:不,我不是个捣乱鬼。
Mat Ryer:好吧,今天和我们一起的还有两位Athens项目的贡献者,我们将会从Aaron Schlesinger和Marwan Sulaiman那里学到很多关于Go模块、依赖项等等的知识。
Marwan Sulaiman:大家好!
Aaron Schlesinger:大家好!
Mat Ryer:我发音正确吗?
Marwan Sulaiman:99%正确。
Mat Ryer:如果不对,欢迎纠正我。
Aaron Schlesinger:对,我也给你99%。
Mat Ryer:哦,99%已经很不错了。我做机器学习的,任何超过80%的准确率都算是很棒了。 [笑声]
Carmen Andoh:人生格言。
Marwan Sulaiman:我们可以聊聊这个话题。
Aaron Schlesinger:对,我们可以聊聊这个。
Mat Ryer:Aaron,你之前上过Go Time,对吧?
Aaron Schlesinger:对,我记得是在2016年,那时候还早呢。
Mat Ryer:你那次聊了什么?
Aaron Schlesinger:我聊了如何教授Go语言,还有“Go in 5 Minutes”项目,以及设计模式。
Mat Ryer:哦,太好了。如果有人感兴趣的话,那期节目应该还可以听吧。其实不用穿越回去,现在就可以听。
Aaron Schlesinger:对,我前几天还查了一下,应该是第18期或者第19期。
Mat Ryer:哦,那还真是早期节目啊。
Aaron Schlesinger:对,我算是元老级别的。
Mat Ryer:太棒了。Marwan,你的背景故事非常有趣,我想……如果是真的话。你能给我们讲讲吗?
Marwan Sulaiman:是的,你就得相信我的话了。我是一名来自伊拉克巴格达的Go开发者。我在1990年代初期长大,这也暴露了我的年龄,但这是故事的一部分……尽管我在独裁政权下长大,我很幸运家里有一台个人电脑。我觉得我妈妈的故事比我的更有趣,因为她在80年代和90年代是伊拉克的COBOL开发者。她的工作是设计数据库,把很多伊拉克的机构从纸质记录转为数字化。
Mat Ryer:哇。
Marwan Sulaiman: 但因为当时美国的制裁,她的开发者工资只有一台个人电脑价格的六分之一---
这是月薪。显然,你得不花一分钱攒六个月的工资才能买得起一台电脑。但她的父亲卖掉了房子,因为他被政权盯上了,另有一段故事……他问他的六个孩子每个人想要什么礼物,我妈妈要了一台电脑。
她父亲买给她的那台电脑就是我小时候用的电脑,它是一台超级老旧的电脑。我到处上网寻找它,问我妈妈:“你还记得它的名字吗?”它是伊拉克组装的版本,后来我查到它其实是NEC PC-6001。在美国它叫做NEC TREK。
它基本上是一个大而笨重的键盘,旁边有一个插卡槽。它带了两张卡带,一张是电子游戏,另一张是BASIC编程语言。我最早的记忆就是让它进入一个无限循环,自恋地让它用不同颜色显示我的名字。 [笑声]
Mat Ryer:听起来很熟悉。
Marwan Sulaiman:从那时起,我一直想成为一名程序员。十年后,第二次海湾战争撕裂了这个国家,2005年,我的家人被恐怖分子盯上了,我的父母想把我送得尽可能远,同时不耽误学业……所以我来到了美国,住在康涅狄格州的一个寄宿家庭里。我在寄宿家庭里上了高中,然后上了大学,学的是国际事务,完全忘记了编程的事。
我不喜欢我的第一份工作,幸好我没有得到我想要的升职,于是我辞职了,想起了我曾经喜欢电脑,然后我加入了纽约市的一个编程训练营,从那时开始,我走上了现在的道路。
Carmen Andoh:哇。
Mat Ryer:你妈妈当时竟然接触电脑,真是令人惊叹。这在任何国家都是了不起的事。
Marwan Sulaiman:是的,非常棒。很有趣的是,我和她重新建立了关于电脑的联系,因为在我重新开始编程之前的10到15年里,我们几乎没有谈过这个话题。她只是在80年代和90年代做过编程,后来就转行了。
最有趣的是,她至今仍不相信COBOL还在用。 [笑声]
Carmen Andoh:但它确实还在用。
Mat Ryer:确实还在用。
Carmen Andoh:真是了不起。我读过很多关于伊拉克文化的东西,很多女性在科学和数学领域有杰出成就---
有一位菲尔兹奖得主(她几年前去世了),就是来自巴格达。我不记得她的名字了,但她曾在斯坦福大学读书。我记得读过她的故事,以及她因癌症去世的悲剧……我想你妈妈应该是她的前一代人,正是她们这一代人才为后来的成就铺平了道路。我很高兴你和你妈妈有这样的共同点。她现在还在伊拉克吗?
(译者注: 实际上是出生于伊朗德黑兰的女数学家 玛丽亚姆·米尔扎哈尼,37岁时获得菲尔兹奖,40岁时去世)
Marwan Sulaiman:是的,她最近在黎巴嫩找到了一份工作,所以现在在黎巴嫩工作。但我家人都还在伊拉克。
Carmen Andoh:哦,明白了。
Mat Ryer:她听起来真是太棒了。
Marwan Sulaiman:是的,谢谢。
Mat Ryer:请代我们向她问好。
Marwan Sulaiman:我一定会的。
Mat Ryer:Aaron,你要不要也分享一下你的背景故事,还是觉得比不上这个?
Aaron Schlesinger:不,我的故事太无聊了……
Mat Ryer:你应该先讲的…… [笑声]
Aaron Schlesinger:我就只是个在芝加哥长大然后搬到这里的人。就这样了。
Mat Ryer:哦,天哪,你说这个话花了不少时间。我有点不爽了。 [笑声]
Aaron Schlesinger:我知道,我应该闭嘴的。
Mat Ryer:不不,请继续。
Aaron Schlesinger:但我想把这个时间用来对Marwan说,我真的很高兴你重新记起自己喜欢编程,并走上这条路,参加了编程训练营,还有后来参与了Athens项目,和我们一起工作。
Marwan Sulaiman: 是的,我也很高兴自己想起来了,因为我真的不喜欢我的第一份工作。
Carmen Andoh:我也很喜欢这个故事,正是一次失败让你走上了这条路。这也许提醒我们,有时候失败对我们的生活来说是件好事。
Mat Ryer:这是个非常好的观点。人们常说,你不会从成功中学到东西,只有从失败中才能真正学到东西……所以你遇到的最聪明的人很可能也是经历过最多失败的人。这有点奇怪。
Carmen Andoh:是的。有一本书叫《Mentors的部落》 ( 中文一般译作 导师部落 ),作者采访了各个领域成功的人士,其中一个问题是:“你最喜欢的失败是什么?”我很喜欢这个问题,因为它拥抱了失败……Marwan,这是不是你最喜欢的失败?
Marwan Sulaiman:我想是的。我觉得不开心是我最喜欢的失败,因为我尽力摆脱它。
Carmen Andoh:这很有趣,因为几周前我们提到Ashley说编程训练营是有欺骗性的,但你就是一个成功的编程训练营案例。
Marwan Sulaiman:是的。有一次,Ashley发推讨论初学者如何学习编程,我建议说“我参加了编程训练营,对我来说效果很好”,然后讨论就变成了截然相反的意见。有些人说训练营是最糟糕的,有些人说是最好的。
我觉得这完全正确。有人运气不好,而我恰好是那些很幸运的人之一,最终很享受这个过程,而且进展顺利。老实说,我没有科学的解释为什么会这样。我所做的就是尽量了解这个训练营,因为我一开始就听说很多公司不信任它们,不知道你学到了多少……对它们有各种成见。所以我真的觉得我走运了。
Aaron Schlesinger:我相信你也付出了很多努力,不只是靠运气……
Marwan Sulaiman:嗯,我参加的训练营有一个有趣的模式,就是你在找到工作之前不需要付钱,这给了你一种安全感,不会觉得自己只是花了很多钱却什么也没学到。他们说:“嘿,如果你最终找不到工作,你就不用付钱。”所以部分原因是他们真的在推动你,确保你学会东西,否则他们就赚不到钱。
Mat Ryer:Marwan,你参加的是哪个训练营?
Marwan Sulaiman:它叫App Academy。这大概是4到5年前的事了。我希望它现在依然像我加入时那样棒。他们在纽约市和旧金山都有分校。
Mat Ryer:好啊,听起来不错。我很感兴趣---
我们肯定会聊到Athens项目,因为我认为它是这个故事中非常重要的一部分---
但也许在此之前,我们可以简单聊聊Go语言和它在依赖管理方面的历史,给那些没有用过Go或者对Go语言比较新的人科普一下。
过去Go的依赖管理主要是通过GOPATH进行的,我觉得它有一个很好的特点,就是导入路径(import path)实际上是项目本身的URL,也就是它托管的地址。这看似简单,但在实际操作中非常有用。每当你查看一个项目的依赖时,你可以直接复制导入路径,粘贴到浏览器中,然后就能打开那个项目。这对我来说是一个非常便捷的认知捷径。
所以当谈到GOPATH时,我的态度还算友好。我知道它对很多人来说都是个棘手的问题,确实也存在一些挑战,当然还有其他问题……但有趣的是,Go团队意识到了这个问题,社区和Go团队也逐渐开始探索不同的想法,看看是否可以用不同的方式处理依赖管理。
有人对这方面的历史有什么看法吗?当时有一些项目---
你们对Vendor文件夹有什么看法?你们觉得这是一个好的进步,还是觉得这是个错误的方向?大家的感觉如何?
Aaron Schlesinger:我插一句……我认为Vendor目录可能是Go在依赖管理方面做出的最关键的改变。比模块(Modules)还要重要,因为正是它开启了我们今天讨论的所有话题。
Mat Ryer:如果有人不知道,vendoring本质上就是在你添加依赖时,直接将其复制到一个名为Vendor的文件夹中。然后Go工具链会首先从这个Vendor文件夹中导入依赖,而不是去网上获取,对吧?
Aaron Schlesinger:对。它不仅开启了这个讨论,还确立了一个查找顺序的概念,当你构建项目时,Go工具会按照这个顺序去查找你所依赖的包。
Mat Ryer:对,会有一个顺序;它会先检查Vendor文件夹,接着可能检查GOPATH,如果找不到,它才会去网上获取原始源代码。
Aaron Schlesinger:对,而且如果我没记错的话,这是第一次开始普遍使用环境变量来定义依赖来源。我记得是在Go 1.5发布后,有一个Go 1.5 Vendor实验环境变量。如果你把它的值设置为“on”,那么Go工具就会首先在本地的Vendor目录中查找依赖。如果找到了,就会从该依赖进行构建,否则它就会回退到GOPATH,最后再去版本控制系统查找。
我记得看到这个环境变量时,我非常喜欢这种做法……有点像Twelve-Factor App的理念,你可以根据项目决定是否使用Vendor目录,或者只是用传统的GOPATH。
Marwan Sulaiman:现在这个选项叫做-mod=vendor
标志,所以你仍然可以选择使用Vendor。
Aaron Schlesinger:对,我对我的Vendor目录感到很舒适。
Marwan Sulaiman:我一直觉得GOPATH很棒,直到你需要版本控制时,事情才开始变得有些复杂。
Mat Ryer:对,没错。最开始你可能没问题---
我自己就经历过这种情况---
我在本地开发环境中使用了某个特定版本的依赖,但与此同时,那个项目在不断变化,而我并没有意识到。当其他人加入并试图构建同样的代码时,他们的机器上没有这个依赖,于是工具就去网上下载最新版本,结果他们的构建失败了,而我的却可以正常运行。这就是这种方式的一个简单缺陷。还有其他问题吗?
Aaron Schlesinger:你刚才说的那个问题,Mat,实际上是我们开始构建Athens的主要原因。因为社区里有太多类似的构建失败问题。
Mat Ryer: 是的。我记得刚开始接触Go的时候,有人问过一个问题:“如果包发生了变化怎么办?”当时的回答是“那就不要改动它们。”听起来有些搞笑,但实际上这个方法确实有效。
Aaron Schlesinger:对。
Mat Ryer:说实话,这个做法确实奏效。
Marwan Sulaiman:永远保持兼容。
Aaron Schlesinger:对,永远。
Mat Ryer:你可以看到,Go核心团队确实这么做了。他们对v1版本有兼容性承诺。而且你提到的-mod=vendor
标志,也说明了他们支持并关心这些问题;他们不想让一切都崩溃。但模块(Modules)会让事情变得复杂吗?对于已经存在的代码项目,将它转为Go模块项目是否很困难?
Marwan Sulaiman:有时候确实会有些麻烦。尤其是如果你的包已经足够成熟,达到或超过了版本2,那么转换为模块不仅对作者来说是个麻烦,对使用该包的用户来说也是个麻烦。因为你必须在导入路径中加入v2或v3。
但如果你还处在0.x或1.x版本,转换为模块其实很容易。你可以保留gopkg.toml
(来自dep的文件),也可以保留任何旧的依赖管理文件。它们可以一起工作,所以你可以同时支持模块和其他旧的依赖管理方式……希望如此。总是会有一些奇怪的边缘情况。
Aaron Schlesinger:对,我认为这种v1的兼容性承诺让Go工具在读取旧的依赖管理文件时表现得还不错,能够将其转换为Go模块格式,也就是go.mod
和go.sum
文件。大致上是80/20的原则,对于很多v1版本的项目来说,它整体上运行得比较顺利。
Mat Ryer:也许你可以多讲讲go.mod
和go.sum
文件的具体内容。它们到底是什么?
Marwan Sulaiman:当然。go.mod
文件基本上定义了你自己的导入路径,如果它看起来像一个URL,那么别人可以通过go get
获取它。这是go.mod
文件的第一行---
定义你是谁,这个程序是什么。如果你正在做一个本地项目,不打算与他人分享,只在自己电脑上使用,那么你不需要用一个完整的URL作为程序的名称或导入路径。
接下来就是第三方依赖及其版本的列表。这是你主要的工作内容,如果你需要第三方依赖的话。从这个项目内部运行go get
时,go.mod
文件会自动更新为Go认为你需要的最新版本。因此,这个文件有趣的是,它是由人和计算机共同管理的。很多时候go get
和go build
会修改go.mod
文件……但有时你需要手动进行一些调整,比如你需要某个依赖的一个分支版本,这时就可以用replace
语句。
有时你可以手动修改这个文件,Go命令可以帮助你完成这项工作,比如你可以运行go.mod replace
命令,实际上就是修改go.mod
文件,告诉Go“我不想要github.com/pkg/errors
的代码,我想用github.com/marwan/errors
,因为我有一些改动还没有合并到上游。”所以你可以深入挖掘。go.mod
文件就是这样……
Mat Ryer:我感觉直接编辑这个文件比用那个replace
工具要容易……你觉得呢?
Marwan Sulaiman:我也是这样做的。我只是想稍微技术一点,告诉大家你可以完全不用手动修改,但实际上你只需要进去加一个replace
语句,复制粘贴导入路径,真的很简单。所以我觉得go.mod
文件很有意思,它体现了人和机器共同协作构建程序的过程,这也证明了依赖管理的复杂性。
这就是go.mod
文件……它还有一个伴侣文件,就是你提到的go.sum
文件。这个文件包含了你所构建模块的完整性信息。这是Go语言与其他编程语言不同的地方。go.sum
文件记录了你所下载的每个模块的校验和。Go会下载模块并将其打包成一个zip文件,然后计算其校验和,并将其记录在go.sum
文件中。这时你可以开始说:“也许我不需要Vendor文件夹了”。你可以稍后通过go get
获取这个依赖,但它必须与最初的版本完全相同,否则Go不会编译你的程序。
Mat Ryer:对,这很有趣。这样你就可以确保在添加依赖时一切都保持不变,对吧?
Marwan Sulaiman:是的。如果不一致,你就无法编译程序。所以这引发了另一个话题:“如果出了问题,我该如何确保程序仍然可以构建?”这时Athens和代理服务器,甚至Vendor目录都可以为你提供保证,确保即使互联网出了问题,你仍然可以编译你的程序。
Mat Ryer:互联网会出问题吗?
Aaron Schlesinger:从来不会。 [笑声] 我从没见过互联网出问题。
Mat Ryer:真的……?
Mat Ryer:那如果一个依赖消失了,或者项目被删除了怎么办?不只是有新版本,而是整个项目都消失了。
Marwan Sulaiman:如果你有Vendor文件夹,那你就没问题。如果你在项目中有这个依赖的副本,你甚至可能不知道它已经在三年前被删除了。但如果你使用的是模块系统,而默认情况下不使用Vendor,它是从隐式变为显式的,你必须明确告诉它“我想从Vendor中构建”---
如果依赖消失了,你就麻烦了。所以你需要一个地方存储每一个你需要的模块的副本。Go团队正在为开源项目开发一个公开的代理……这是公有部分,但对于私有模块呢?这时候就轮到Athens项目出场了。无论是公有还是私有,这两者都试图解决“当模块消失时,我们不希望整个互联网崩溃”的问题。并且因为我们依赖的是版本控制系统,而不是像npm或RubyGems那样的注册表,所以没有任何约束;任何人都可以删除他们的代码库。他们有权这么做,这是他们的代码;他们可以创建它,也可以删除它或修改它。
Aaron Schlesinger:Marwan,除了你刚才说的,我觉得还有一个很酷的地方,就是我们现在可以把开发者的工作流和我们真正用来构建程序的代码分开。我觉得在CI/CD世界里,有时候人们会把源码和发布资产分开。如果你稍微眯起眼来看,这就像是在模块生态系统中,我们将源码视为源码,而模块则是真正被应用开发者使用的工件。我认为这是一个巨大的进步。
Marwan Sulaiman:没错。一切都从源代码本身开始,这也是Go生态系统最棒的部分。最终,真相的来源就是你的源代码。所以你不必像在npm里那样发布一个包,可能忘了推送到GitHub。或者你推送到GitHub,但忘了发布到注册表。所以一切都从---
真相的来源就是你的GitHub仓库。但一旦代理服务器复制了它,它就不再是“消失的”。
Carmen Andoh:是的,当软件进入生产环境后,它就成了下一个逻辑操作单元。我很喜欢你提到我们一直以来依赖版本控制系统来管理依赖,而这在过去几年里带来了很多后果……但我想到了Node.js的left-pad事件,这是一个著名的依赖失效事件,对吧?当一个依赖不再可用时,很多应用程序都崩溃了。如果当时他们有一个代理服务器,这些问题就不会发生了。
Mat Ryer:我倒是挺喜欢这个left-pad事件的故事。
Marwan Sulaiman:某些公司可能会因此省钱。 [笑声]
Aaron Schlesinger:Carmen,你提到的这个概念其实也引出“联邦”体系的讨论,这也是模块生态系统中的一个很酷的东西。像你说的,这在npm生态系统中肯定会有帮助……但让我感到欣慰的是,Go模块生态系统并不依赖于某一台服务器。任何人都可以运行一台服务器,实际上已经有多个公共代理服务器了,你也可以自己运行代理服务器。这一切都可以很好地协同工作,这对我来说真的很酷。
Carmen Andoh: 我同意。我记得Athens刚出来的时候,我特别喜欢它。当时有一个白皮书,介绍了模块基于的协议,允许社区根据自己的需求构建解决方案。
然后我看到了Athens项目,我记得那时---
我们在波特兰的一个聚会上,你做了一个关于Athens的演讲,我当时就想偷点时间来贡献代码……不过那段时间很短暂。[笑] 但我真的很喜欢你们俩的工作。当然,还有Manu和很多其他人。GopherSlack的Athens频道非常棒……非常欢迎新手,非常活跃,大家都很乐于助人……我特别喜欢这个社区。
Aaron Schlesinger:我们欢迎所有人,Carmen,如果你想回来,我们随时欢迎你。
Marwan Sulaiman:我们是那种让人烦的友好。
Carmen Andoh:对,Marwan,说得太对了,确实是这种感觉!
Aaron Schlesinger:Marwan,你应该说说你的那个“轻松开源”的口号。
Marwan Sulaiman:哦,天哪……对,我记得有一次Carolyn说:“我需要在周五之前推送这个改动,再发布。”我说:“你可以随时做这件事。如果你有空,当然可以这么做,否则这只是一个轻松的开源项目。没有任何期望。你能参与对我们来说已经很重要了。”
Carmen Andoh:[笑] 我喜欢这个!这应该成为我们的口号。当然,还有“让人烦的友好”。这也是个好口号。我特别喜欢你们会认真对待所有问题。你们真的是开源项目中应该有的榜样。
Marwan Sulaiman:谢谢。
Mat Ryer:任何在运行开源项目的人,都应该看看Athens,看看能学到什么。
Aaron Schlesinger:或者直接来跟我们一起玩。
Mat Ryer:那么Athens的历史是怎样的?它是如何开始的,又是谁发起的?
Aaron Schlesinger: 当 Russ 发布的那一系列博客文章---
或者说是文章吧,我不太清楚具体的术语是什么---
刚出来的时候,里面有一部分关于下载协议的内容。当时,它看起来和现在的差不多。这个协议的核心是大概五六个 HTTP 端点,可能是五个。基本上,任何人都可以像 Carmen 你说的那样构建这个协议,因为它是一个抽象层,你可以在背后放置任何你想要的东西。
我写了一个 Buffalo 应用程序---
这儿向今天不在的 Mark 致敬---
这个应用程序基本上实现了这个协议,它在从 GitHub 或其他地方抓取模块后将它们存储在内存中。如果你运行 go get
来请求 Athens 代理,它会在后台执行自己的 go get
,回到 GitHub 或其他地方获取模块,然后将其存储在内存数据库中。下次你运行 go get
时,它会直接从内存中服务该模块。这个项目基本上是个玩具,但我把它展示给了几个人---
Marwan 提到的 Carolyn van Slyck,还有 Erik St. Martin 和 Brian Ketelsen。如果我忘记了谁,我很抱歉,但我想这几乎是所有人了。他们对这个项目也非常感兴趣,所以我们决定一起合作。
我们创建了一个新的 GitHub 组织来托管这些代码。我把代码命名为“vgo prox”,就像 vgo 代理一样,虽然这名字非常糟糕……所以 Brian 去了一个创业公司名称生成器,因为命名总是很难……[笑] 我们最终得到了一个叫“Athens-Brass”的名字,于是我们决定采用希腊主题,并把它叫做 Athens。这个项目就这样诞生了。
后来 Brian 做了几次演讲,我也做了几次小型的活动,比如见面会。起初参与的人很少,但后来……我不好说我们是否有大量的人涌入,但比起最初的零星参与者,现在已经有更多人加入了。显然,我们现在有了非常棒的贡献者,比如 Marwan,还有其他很多人。我想现在有大约六个核心维护者,还有大概十五到二十个官方贡献者在 GitHub 组织中。除此之外,还有很多其他人,他们可能会进来打个招呼,或者修复文档、修复 bug 等等。
我觉得,任何人只要进来打个招呼,或者做得更多一点,我个人都会认为他们是 Athens 的一部分。如果他们进来问候一下,那就和修复 bug 一样好,因为他们已经是社区的一部分了。如果他们想做更多的事情,我们也会帮助他们。
我又跑题了,开始谈论社区了,但希望这能为你们提供一点历史背景。
Mat Ryer: 不,这很棒。这真的很有趣,我最喜欢的是这些项目是为了解决实际的问题而诞生的。太多技术项目---
我们都难免会犯这样的错---
我们会想象出一些很酷的东西,然后几乎虚构出一些问题来为这些酷炫的解决方案服务……但当有一个直接且明确的问题摆在眼前时,我真的很喜欢这种情况。
我认为每个开发者在工作时都需要知道他们的目标是什么。很容易迷失在抽象的技术中,或者在大型组织中迷失方向,但如果你失去了“为什么”去做这件事的理由,我认为你就会遇到麻烦……所以我总是敦促大家记住这一点,而 Athens 项目就是一个好例子。这里有一个明确的问题,一个我们长期以来一直感受到的痛点,然后人们开始围绕它展开讨论。我觉得这就是我喜欢开源的原因。
顺便提一下,Athens 项目在 GitHub 上现在有 2,200 颗星,这相当令人印象深刻。虽然我们不以星星来衡量项目的好坏,但它确实有那么多星星。
Marwan Sulaiman: 其中一半是我创建的机器人点的……[笑]
Aaron Schlesinger: 另一半一定是我创建的机器人点的。干得好,Marwan!功劳属于你![笑]
Mat Ryer: 我创建的机器人不工作,所以这些星星中没有我的贡献。 [笑] 那么关于速度呢?其实我先问你这个问题---
当一个模块被创建或依赖项被克隆时,\_test.go 文件会发生什么?
Marwan Sulaiman: 这取决于模块是何时创建的。如果你正在构建自己的项目并导入了一个依赖项,不管是 Athens 代理还是其他代理,当 Go 将该模块添加到你的 go.mod
文件时,它不会添加任何测试依赖项。此时你需要在命令行中输入一个小小的魔法命令,叫做 go mod tidy
。go mod tidy
会整理你的整个 go.mod
文件,移除你不需要的依赖项,同时也会添加所有的测试依赖项,这样当你运行依赖项的测试时,你会确保有正确的依赖项,或者基本上确保你的测试是可复现的。
Mat Ryer: 那么,测试文件会随着代理一起传递到 Athens 吗?当 Athens 复制模块时,是否会复制整个模块?
Marwan Sulaiman: 是的。有趣的是,旧的代理必须使用 Go 命令本身来下载模块。它们不一定要这么做,但如果不这么做,那它们就有点“离经叛道”了。举个例子,假设你正在从 Athens 构建某个东西。你在本地计算机上,有一个 Athens 服务器在某处,你说“嗨,我能得到包裹一号吗?”如果 Athens 没有包裹一号,它将不得不从某个地方获取它。它可以去 GitHub 自己下载,然后返回给你,但这样做的问题是你可能会跳过某个字节,导致你的校验和与另一个代理的校验和完全不同,或者与原始 Go 校验和不同。
因此,当你下载模块时,或者当 Athens 下载模块时,它必须使用一个叫做 go mod download
的 Go 命令。我相信 go mod download
---
如果我说错了请纠正我---
基本上会下载整个代码库并生成一个 zip 文件。它有一些规则;我想它会跳过符号链接和其他一些东西,但它会保留大多数文件,并生成一个 zip 文件。
所以这是一种很好的抽象,Athens 甚至不需要去思考这些问题。只要它有 Go 命令,它调用 go mod download
,它就会为你下载所有文件,而我们只需要存储 go mod download
在磁盘上生成的内容就行了。
Mat Ryer: 如果代码库中有一张图片,然后有人修改了那张图片,校验和会因此不同吗?即使代码本身没有变化?
Marwan Sulaiman: 应该会。
Aaron Schlesinger: 我想校验和只针对代码部分,对吗,Marwan?
Marwan Sulaiman: 这是个好问题。我记得我曾经试验过。我浏览过一些 Go 模块的代码,里面有很多东西……我记得它跳过了一些东西,但我不记得它跳过了哪些文件,只关注代码部分,但我可能错了。
Mat Ryer: 有趣。
Marwan Sulaiman: 我会在我们聊天时偷偷做一个小实验。
Aaron Schlesinger: 如果频道里有人知道答案,可以贴出来。
Mat Ryer: 当他们引入构建缓存时,我注意到构建速度变快了很多。使用 Athens 和 Go 模块后,速度会有提升吗?
Marwan Sulaiman: 使用 HTTP 协议下载模块---
无论是 Athens 还是其他代理---
的有趣之处在于,你不再使用基于 Git 或版本控制系统的方式下载模块。想象一下,如果你依赖 CockroachDB,你想下载它。旧的方式会执行 Git 克隆,这意味着它会下载整个历史记录。但使用新的代理,你知道你想要的具体版本,所以你只需要该代码库的 zip 格式状态,你不需要下载整个版本控制系统的历史。这是一个巨大的性能提升。一旦你将它存储起来,性能会更高,因为你不需要再从互联网上获取它。
性能方面---
我记得最初的 vgo 文章中提到了 CockroachDB 作为例子,表示下载时间从 4 分钟缩短到了几秒钟,这差别很大。
Mat Ryer: 哇,这太酷了……尤其是在那些互联网连接不太好的国家或城市,这显得尤为重要。我知道有一些开发者面临这种情况,网络质量不佳。这对构建和依赖管理来说,确实会带来实际的影响。这真的很棒。
Aaron Schlesinger: 还有一个方面是,因为它是 HTTP 协议,所以它是 Web 协议,你可以对它进行缓存,还可以在它前面放置 CDN,做所有 Web 开发者熟悉的酷炫操作。我不清楚现在一些公共代理的具体细节,但如果让我猜,我会说它们可能会与 CDN 一起运行,这意味着当你使用这些代理执行 go get
时,你会从离你非常近的地方获取一个非常小的 zip 文件,而这个地方的带宽非常好。
Mat Ryer: 太棒了,这真是太酷了。
Carmen Andoh: 是的……Mat,你之前提到我们能够找到问题的原因,记住原因并解决问题,我认为你刚刚定义了一个健康生态系统的特征。而 Athens 项目就是一个典型的例子,它展示了依赖管理在分布式系统和当今软件开发中的重要性,以及社区和生态系统如何共同解决这个问题。
Mat Ryer: 是的,我非常喜欢这一点。
Marwan Sulaiman: 我最喜欢的一点是你们刚刚提到的所有内容,以及你可以扩展基础协议的事实。对我来说,这就是我发现 Athens 项目最有趣的地方。你们提到了速度,也提到了这种需求,然后你可以说“等等……现在我有了所有模块的存储”,所以你可以想到也许某个公司想要为他们的所有项目运行一个 Athens 服务器,这个服务器的存储中有他们所依赖的所有模块。对我来说,这是代理与 vendoring(供应链管理)的最大区别。
如果每个项目都为自己的依赖项提供供应链管理,你实际上并不知道每个项目需要什么。你必须进入每个项目的供应链管理目录中查看它依赖的是什么。但现在你有了一个集中的地方来存储这个公司所有项目的模块,你可以做一些事情,比如扫描这些模块,查看是否有漏洞项目,或者集成第三方安全软件。你可以做各种各样的事情,你基本上可以从这个集中存储中构建任何你想要的东西。
Carmen Andoh: 更不用说安全方面的影响了,对吧?当你为它们建立索引时,你会给它们加上 SHA 或某种哈希值,确保任何可能的攻击向量都被排除在外,因为你有索引,你知道你获取的包就是你想要的包。我不确定这是否会成为一个可能的漏洞,或者是否曾经有过这样的漏洞;我不清楚。但我知道有时我会问“如果我不是通过 SSH 进行操作,我如何知道我获取的东西没有在中间被篡改?”版本控制确实有助于解决这个问题,因为它们有提交 SHA 值和类似的信息,但它们不会对 GitHub 之外的对象和标签进行处理。所以,如果你不使用 GitHub 或 GitLab,我会经常对此感到疑惑。
Aaron Schlesinger: 是的,关于安全性和完整性,在整个模块生态系统中,这个“故事”有很多很酷的层面。我把这些内部组织的东西称为“企业级”特性。这是一个非常技术性的术语。 [笑] 但 Marwan,你和 Carmen 也提到了这一点---
你有能力控制入口点,而不是依赖 GitHub 或版本控制系统为你进行模块认证。除此之外,Go 团队还通过让你在获取模块之前就能验证并证明该模块的校验和有一个审计轨迹,这样就增加了另一层可审计性。
因此,在代码进入你的代码库之前,你就可以证明该代码没有被篡改……之后,从代码进入你的代码库的那一刻起,你始终有这些校验和可以查看,系统会自动阻止构建失败,所有这些很棒的功能也是如此。
这也是对社区的另一种证明,我们有了这些开放的协议,现在我们可以在安全领域构建多个不同的层次,我觉得这真的很酷。
Mat Ryer: 那么我们今天如何使用 Athens 呢?你是使用 Go 代理环境变量,还是还没有托管?它是公开可用的吗?目前的情况是什么?
Aaron Schlesinger: Athens 主要是为内部托管或作为你自己的镜像使用的。很多人都在他们的 CI 流水线中运行它。我知道有些人在非常受监管的公司中运行它,这些公司基本上关闭了对互联网的访问。
我知道有一个人---
这是几个月前的事情---
他在一个组织中运行 Athens,在那里你必须通过 U 盘传输代码,并且得到法律部门的批准,然后你才能将代码加载到 Athens 中。
这是它的主要使用场景……但我自己,还有一些其他人,我们只是喜欢玩它。我把它托管在云上,然后我做一些奇怪的事情,构建一些愚蠢的扩展,诸如此类。
我主持的其中一个实例在我们的文档页面上,你可以试一试。你不需要自己设置 Athens,只需将 go proxy
设置为这个地址,然后开始使用 Athens。但我们也有很多关于如何安装它的指导,从简单的 Go 二进制文件到在 Kubernetes 中运行它的各种步骤。
Mat Ryer: 这太棒了。网站也非常棒。我建议有兴趣的人……是 docs.gomods.io 吗?
Aaron Schlesinger: 是的,没错。
Mat Ryer: 我建议有兴趣的人去看看。
Marwan Sulaiman: 是的。如果有人想下载并使用 Athens,请访问 docs.gomods.io,还可以去 Athens 的 Slack 频道,你可能会很快得到回复。
Carmen Andoh: Athens 的 Slack 频道在 GopherSlack 上非常活跃,非常有帮助。
Mat Ryer: 太棒了。在构建 Athens 的过程中,有没有遇到什么意外?
Mat Ryer: 太棒了。在构建 Athens 的过程中,有没有遇到什么意外的情况?因为我觉得它的很多价值其实来自于它的设计和背后的思考---
当然这只是我的看法,可能不对。但在技术实现上是否困难?在构建过程中有遇到什么意外的情况吗?
Aaron Schlesinger: 意外的情况……让我想想有没有什么实际的例子……因为我现在只记得当时常常会说“嗯,我没想到这一点”,但我不太记得具体是什么了……
Marwan Sulaiman: 我有几个例子。
Mat Ryer: 你们没有用依赖管理工具吗?[笑声]
Aaron Schlesinger: 我不说。
Marwan Sulaiman: 截至今天,它仍然使用 Vendor,我们离不使用 Vendor 也很近了,这确实挺有趣的。就像我们在构建一个基于新模块系统的项目,但我们自己却不完全信任这个新系统一样。[笑声]
Aaron Schlesinger: 能把这一段剪掉吗?[笑声]
Mat Ryer: 当我说了些不该说的话,它就会自动剪掉。
Marwan Sulaiman: 是的,这确实很有趣。
Aaron Schlesinger: 只是开玩笑。[笑声]
Marwan Sulaiman: 说实话,我很喜欢我们仍在使用 Vendor,现在才考虑移除它。因为 Vendor 是个已经被验证并使用了很长时间的工具,而模块系统还很新,它将在下一个版本中默认启用。所以我觉得这是个非常“成熟”的决定。我们对新系统很兴奋,但同时也非常谨慎,确保它真的好用。只有当我们觉得自己构建的东西足够好时,才会用 Athens 来构建 Athens。我觉得我们现在已经到了这个阶段。
Mat Ryer: 我觉得这很合理,毕竟以前 Go 也是用 C 写的。
Marwan Sulaiman: 完全正确。
Mat Ryer: 所以这其实是一样的。
Aaron Schlesinger: 我觉得还有一点,当 Go 刚开源时,大家呼吁用不同的方式实现 Go 规范。现在我们希望社区也能这样做,事实上也确实如此。现在有 proxy.golang.org,有 Athens,有 GoCenter.io... 我还知道有一个叫 GoProxy 的项目在 GitHub 上。有人给我展示了一个用 Bash 构建的代理……
Carmen Andoh: 嗯……
Aaron Schlesinger: 这让我大吃一惊。
Mat Ryer: 什么?!
Aaron Schlesinger: 如果你是用 Bash 构建它的人,能不能把链接发到 GoTime.fm 频道?因为那东西真酷……
Mat Ryer: 但它是一个 HTTP 服务器,对吧?
Aaron Schlesinger: 是的,显然你可以用 Bash 来做这个……
Mat Ryer: 什么!?这让我大为震惊。
Marwan Sulaiman: 对,各种很棒的实现。而我最喜欢的是,在 1.13 版本中,GoProxy 环境变量将变成逗号分隔的参数或值,这样你就可以告诉 Go 使用多个代理来构建项目。这带来了各种很酷的可能性,比如你可以优先使用内部代理,它只存储你的内部代码,对于任何公共代码,你可以告诉它“返回 404”,然后 Go 会转到下一个代理。也许下一个代理是 proxy.golang.org,但如果它挂了,可能也会返回 404,然后继续寻找下一个代理……你可以确保高可用性,只要代理能保证这一点,你可以从客户端侧实现各种不同的逻辑。
Mat Ryer: 你知道他们是否支持 ETags
和 If-Match 头
等标头吗?这样你可以说“如果依赖项发生了变化,给我新的版本;如果没有……”还是说这不适用,因为你已经在请求特定版本了?
Marwan Sulaiman: 当你第一次下载模块时,你并不一定是在请求特定版本。Go 下载协议有五个不同的端点,其中一个端点是发现端点,它会告诉你“对于这个模块,你有哪些版本?”这就是 v/list 端点。所以当你请求“gopkg/errors/v/list”时,你可能会得到一个语义版本的列表,如果仓库没有语义版本,它会转到下一个发现端点 @latest。它会说:“好吧,如果你没有语义版本,就给我最新的版本”,这可能类似于 Git 提交类型的伪语义版本。话虽如此,目前还没有办法支持---
其实我不太熟悉 ETags,但我知道目前没有支持任何特殊的标头,我可以进一步解释……但你能先解释一下 ETags 吗?
Mat Ryer: 你只需发送一个字符串,然后服务器决定是否基于此有更新版本,并设置 ETag 标头。这只是一个缓存机制,我只是好奇它是否在这个过程中起作用,或者是否可能起作用……但这挺有趣的。
我还在想,如果有人拥有一个 GitHub 仓库,或者他们维护一个项目,现在有些什么是他们应该注意的,而以前不那么重要的?我特别想到的是标记发布版本之类的事情……但有没有其他好的做法和建议?
Aaron Schlesinger: 是的……语义版本管理(Semver)。你提到了标记发布版本,但模块系统非常认真对待语义版本管理。Marwan 在节目一开始就提到了这一点---
当模块检测到你提升了主要语义版本时,它实际上会要求你更新路径。所谓模块路径,我的意思是如果你从 V1 升级到 V2(这是一个重大变化),你的模块路径会变成 /v2。如果你打算做 GitHub 或 Git 标签,你真的需要注意你是否做了一个破坏性的更改,如果是,你需要知道这意味着那些想获取你破坏性更改的人需要更新他们的导入路径,添加 /v2 到路径中。
Marwan Sulaiman: 补充一下,我相信 Go 团队正在构建一个工具,它可以帮助你检测是否做了一个破坏性更改---
至少在 API 签名方面,比如函数和类型签名---
它会给你一个警告,这样你就知道你不应该将新发布的版本标记为次版本或修补版本,而是应该标记为主要版本。我不记得那段代码在哪,但我相信它是在 Experimental 或 x/tools 中。
Mat Ryer: 真遗憾,因为我刚刚想到这个主意。[笑声] 不过我也不感到羞愧……
Marwan Sulaiman: 我记得 GopherCon 有个关于这个的演讲……
Mat Ryer: 是的,当然。很多这样的工具现在变得可能了。像依赖图这样的工具,现在也更容易编写。
Carmen Andoh: 我喜欢你提到的那个观点,语义版本管理(Semver)现在---
对于那些原本不太认真对待语义版本管理的人,随着模块系统在 1.13 中默认启用,人们将不得不非常认真地对待语义版本管理。我至少知道自己在次版本发布时并没有太在意……但我觉得这是另一个好处。
Aaron Schlesinger: 是的,我也不例外。我发布 V2 的唯一时候就是创建一个新仓库,因为我根本不知道该怎么做……[笑声] 我现在基本上是这样:如果感觉改动不大,我就做个修补版本;如果感觉比较大,我就做个次版本。我尽量不破坏任何东西。大概当我们发布 Athens 的 v1.0.0 时,我希望有其他贡献者比我更懂语义版本管理。
Marwan Sulaiman: 我最喜欢的例子是 Twirp 框架,它正在尝试升级或迁移到模块系统。他们已经到了第 5 版,所以现在正在讨论如何升级到第 6 版并引入模块系统,同时还要支持那些不使用模块系统的用户……所以当你想确保向后兼容时,这确实是一个复杂的话题。
Mat Ryer: 是的,很有趣。Mark Bates 曾经给我发过一条消息,他说:“我们不能再做朋友了,因为你没有在这个仓库中标记发布版本。”[笑声] 就这样。从那以后我再也没收到过他的消息。[笑声]
Carmen Andoh: 所以他今天没有上节目就是这个原因。
Aaron Schlesinger: 哦,你把他赶走了。[笑声]
Mat Ryer: 是的。
Carmen Andoh: 所以这个故事的道德是“标记发布版本”。
Mat Ryer: 是的,用语义版本管理。
Carmen Andoh: 用语义版本管理(Semver)。
Aaron Schlesinger: 这确实让事情变得更简单,当你在 go.mod 文件中看到 v1.1.1 之类的版本,而不是一长串提交哈希时,感觉要好得多。对于社区来说,以这种方式共享代码更有意义,因为它为人阅读提供了更多信息,而不只是一个你必须去 GitHub 上查找的哈希值。
Mat Ryer: 是的,这也是一种很好的做法。如果我们回想 Go 的 1.0 承诺---
“从这一点开始,一切都向后兼容”,当你第一次标记 v1.0.0 版本时,这是一个重要的事件,感觉更像是一个里程碑。而如果你根本不在意这些,可能你觉得自己已经到了版本 1,但实际上正式标记这个版本是一个很好的事件,你可以期待这个时刻。
我特别喜欢 Buffalo 项目,它运行了好几年,但还没有到版本 1,原因是一样的;一旦他们发布 1.0 版本,他们会确保所有东西都能正常工作。这是我认为帮助 Go 获得广泛采用的原因之一,也使得 Go 成为我最喜欢的工具和编程语言之一……因为我可以依赖它。
Aaron Schlesinger: 是的,关于 1.0 之前版本的讨论也很有趣……因为模块系统假设任何 1.0 之前的版本随时可能会发生破坏性变更。所以如果你是 v0.xx 的版本,那就向模块生态系统中的人们发出了一个信号,表明这个版本随时可能发生破坏性变更。这有点像 1.0 承诺的逆向思维……因为当你到了 1.0 版本,你就知道它是稳定的。
Mat Ryer: 是的。所以当你说 Go 模块知道这些东西可能随时会发生破坏性变更时,它会如何处理这些信息呢?
Aaron Schlesinger: 嗯,这回到我之前提到的路径问题:当你从 1.0 升级到 2.0,或者从 2.0 升级到 3.0 时……这就是你需要更新路径到 /v2 或 /v3 的时候。
Mat Ryer: 明白了。
Aaron Schlesinger: 但当你从 v0.x 版本升级到 v1 时,你不需要在路径中进行更改,因为默认假设当你发布 v1 时,路径会是 github.com/mypackage
。从那时起,直到你发布 v2 版本之前,你都可以更新次版本和修补版本,任何依赖这个包的人都不应该遇到任何兼容性问题,显然这是默认的期望。
但是他们不会把从 v0.x 到 v1 看作是一个重大事件,因为默认假设当你从 v0.x 升级到 v1 时,所有东西都会发生不兼容的变化。
Mat Ryer: [01:00:05.16] 是的,这很合理,太棒了。实际上,这完全符合我们构建项目的方式,不是吗?这符合现实情况,因为在 v1 之前,版本是相对灵活的……我喜欢这一点。我喜欢它能够理解社区已经在怎样做事的现实。
Aaron Schlesinger: 是的,它对开发者也是很友好的。
Marwan Sulaiman: 是的。而且,语义版本管理(semver)本质上---
嗯,不是完全,但大部分是人类之间的契约……你可以尽可能多地让计算机识别 API 签名的变化,正如我之前提到的,但归根结底,计算机无法很出色地识别行为上的变化。而行为变化也是 API 稳定性或兼容性契约的一部分……所以当你说“我改变了语义版本,这是一个新版本”时,这实际上是个人的决定。即使整个 API 都没有改变,但如果行为发生了变化,你也应该更改主要版本。
Mat Ryer: 对,没错。
Carmen Andoh: 今天 Marwan 说得太棒了……“语义版本管理主要是人类之间的契约”,确实如此。
Aaron Schlesinger: 是的……
Marwan Sulaiman: 我把这些都记下来了,我还有几个不错的句子……[笑声]
Mat Ryer: 你把它们都说出来,我们会挑选最好的。
Marwan Sulaiman: 我有一个叫“好句子”的文档。[笑声]
Carmen Andoh: 我需要。我需要这个文档。快给我。[笑声]
Mat Ryer: Ian Molly 在 Slack 上说,“好句子”应该拼成 b-y-t-e-s(字节)。
Marwan Sulaiman: 哇,太棒了。
Carmen Andoh: 我也是这么拼的。在 Go Time 中没有其他拼法吧?只能是 b-y-t-e-s……
Mat Ryer: 我问你们一个问题……你们知道 4 位二进制数叫什么吗?8 位叫一个字节,对吧?
Aaron Schlesinger: 半个字节?
Marwan Sulaiman: 不知道,是什么?
Carmen Andoh: 哦……这是个单位吗?
Mat Ryer: 我觉得是的,因为我记得有这么个东西,但我不记得它具体叫什么了,我也从来没查证过这个东西,直到刚才。
Marwan Sulaiman: 我有点紧张,这感觉像是面试问题。
Carmen Andoh: [笑声] 那是什么?
Mat Ryer: 我们稍后告诉你……[笑声]
Aaron Schlesinger: 有人把这个很棒的答案发到频道里了。
Carmen Andoh: 我不想透露答案---
Mat Ryer: 这是正确的,它叫 nibble(半字节)。我当时也以为是这个。
Aaron Schlesinger: 哇,太酷了!
Marwan Sulaiman: 太棒了!
Aaron Schlesinger: 我喜欢这个!
Mat Ryer: 是吗?我不知道 Ian 是不是在开玩笑……
Carmen Andoh: N-i-b-b-l-e?哦,我的天……有人能验证一下吗?快给我个链接。我们要查一下这个 nibble。
Mat Ryer: 是的,已经验证了。
Carmen Andoh: 哦!验证了……不过这是 Wikipedia。我不知道……
Aaron Schlesinger: 但它这么“极客”,应该是真的。
Carmen Andoh: 我知道,真的很 nerd(极客),我真心希望它是真的。
Aaron Schlesinger: 我也是。[笑声]
Mat Ryer: 我们可以让任何事情都变成真的,只要我们都说它是真的。很简单。
Aaron Schlesinger: 嗯,好吧,听起来不错。
Carmen Andoh: 哦,好吧……
Mat Ryer: 是的。Ian 在 Slack 上说他拼错了,但实际上如果你去看,确实有一种拼法是“nybble”。
Carmen Andoh: 太酷了……[笑声]
Mat Ryer: 很好。
Aaron Schlesinger: 我是唯一一个觉得用 y 拼写的 nybble 比用 i 拼写的更 nerd 吗?
Carmen Andoh: 是的,那才是最好的拼法。
Mat Ryer: 没错。
Aaron Schlesinger: 嗯,太好了。
Mat Ryer: 你都已经叫 4 位二进制数为 nibble 了……你懂我的意思吧?你已经注定不会在大多数派对上受欢迎了,何不干脆换个 y?
Aaron Schlesinger: [笑声] 对!要么做大,要么回家!
Mat Ryer: 没错……
Marwan Sulaiman: [无法听清 01:03:23.02]
Mat Ryer: 或者直接回家。拜托,赶紧回家。这是他们在派对上对我说的,不是对你。我现在不是在对你说,是他们对我说。[笑声]
Aaron Schlesinger: 这很公平。
Mat Ryer: 是的。非常感谢你们。我觉得这期节目很棒。依赖管理在任何语言中都是一场噩梦,Go 之前有 GOPATH,我们也勉强用它,但它绝对不是最好的解决方案,使用起来也不太舒服。Go Modules 看起来是朝正确方向迈出的一步,它似乎真的能帮助我们。
当然,依赖代理等工具也能帮上忙。如果你在自己的环境中需要这些工具,去看看 Athens 项目吧。我相信你会喜欢的。
这就是本期 Go Time 的全部内容。我们下周再见!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。