CarlSpotlight PA 技术总监)和 Wayne(GoDaddy 首席工程师)与 MatMark 一起讨论 Go 1.16 中的新 go:embed 功能。他们讨论了如何使用以及何时使用、需要注意的常见问题,以及一些相当有分量的不受欢迎的观点。

本篇内容是根据2021年3月份#171 go:embed音频录制内容的整理与翻译

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




Mat Ryer:大家好,欢迎来到Go Time。我是Mat Ryer,我认为应该默认可以粘贴不带格式的内容。我不觉得我应该把手扭成某种奇怪的姿势来复制粘贴,以避免文本匹配源格式。我从来就不想要这个。

Mark Bates:我相信这个键位组合其实是EMACS里的保存快捷键。

Mat Ryer:哦,那这简直就是灾难的前奏,不是吗?幸好我用的是VS Code。好吧,大家已经听到那个提前发声的人了,虽然还没介绍他,这明显违反了规则,但这也让大家对他有了些了解---哦,嗨Mark!欢迎回来,Mark Bates!

Mark Bates:男人、神话、传奇。

Mat Ryer:是啊,你是个男人,对吧?至少是三分之一的人……

Mark Bates:不,其实我是个神话。

Mat Ryer:哦,真的吗?

Mark Bates:我全是神话,纯粹的神话。

Mat Ryer: [笑] 好吧,希望我们这里也有一些真实的人……让我们认识一下他们吧。今天还有Wayne Ashley Berry加入我们。你好,Wayne是GoDaddy的首席工程师……Wayne,你是个艺术家,对吧?欢迎来到节目。

Wayne Ashley Berry:谢谢邀请。我是这个节目的长期听众,非常高兴能来参加。

Mat Ryer:非常欢迎你。你主要做什么类型的艺术?

Wayne Ashley Berry:我一开始是画画的,后来我接触了计算机图形学,从那以后就一发不可收拾了……其实这也是我最初学习编程的原因,所以这就是我为什么在这里。

Mat Ryer:哦,太酷了……希望我们能看到一些你的作品;当然,不是在播客里,但或许你可以在节目笔记里分享你的Twitter账号。我们今天还有另一位嘉宾Carl Johnson。Carl是一位拥有哲学博士学位的软件工程师,现任Spotlight PA的技术总监。对吧,Carl?欢迎加入节目。

Carl Johnson:没错,嗨。感谢邀请我。

Mat Ryer:不,感谢你能来。今天我们要谈论的是//go:embed。这是Go 1.16中的一个新功能,它允许你把文件嵌入到二进制文件中。但为什么你会想这么做呢? 谁能告诉我们?

Mark Bates:为什么你不想这么做呢?

Mat Ryer:那它为什么有用呢?

Mark Bates:这是个好问题。我们多年来一直在用各种方式来实现这一点,所以现在终于有了这个功能,真是太棒了。这个需求从一开始就存在了,所以我得先说一下我对这个发布版本的兴奋之情,我对这个嵌入功能非常兴奋……我们为什么想这么做呢?原因有很多。大家最常提到的一个例子是Web应用。如果你想构建Web应用,Web应用里有什么?有图片、样式表、JS文件、模板……如果能把这些都自包含起来,而不需要把这些文件放在磁盘上管理,那不是很好吗?这是其中一个原因,也是一个典型的使用场景。

我一开始做Buffalo框架的时候,就提到过它是从一个实际项目中提取出来的……而在那个项目里,把这些文件嵌入到二进制文件里是必要的,因为他们需要能够打包好一个包含所有内容的二进制文件。所以正如我所说,这是我们社区一直存在的问题,这可以应用到任何类型的应用程序上,但最典型的还是Web应用。

Mat Ryer:所以关键点是,你可以把本来需要单独管理和部署的文件放到二进制文件里。那它只是为了让部署更简单吗?这是主要原因吗?

Mark Bates:这肯定是最主要的原因……因为多年来我们有各种解决方案,有些让问题变得很麻烦,有些则比较透明。有些方式要求你在编译时就把文件嵌入进去,而那样做起来很麻烦,但你这么做是为了获得更简单的部署方式。幸运的是,在新解决方案中,像很多其他方式一样,它变得更加透明……

所以,是的,你希望能轻松地部署。这样你可以打包一个包含所有内容的二进制文件,不管是用来部署到你的Web服务上,还是发给客户。你可以把所有的迁移文件都打包进去……一切都打包得紧凑整洁。它更简单、更干净,只需要一个文件。

Carl Johnson:另一个例子是由Steve Francia创建的Hugo静态网站生成器……它有内部模板。它是一个用来创建你自己网站的工具,你提供模板并告诉它如何生成你的网站。但它也有一些内部模板。现在,如果你查看它的源代码,你会发现它有HTML文件放在一处,然后还有一个Go文件,内容几乎完全一样,上面还写着“自动生成,请勿编辑”。他们必须确保两个文件同步更新,每次内部文件有修改,他们就得更新对应的Go文件。

Mat Ryer: 他们大概有某种脚本可以自动帮他们做这件事,对吧?

Carl Johnson是的,他们有一个go generate脚本来处理这个。所以这是一个例子,Hugo的二进制文件只是一个单一的可执行文件,你可以在Mac、Windows或Linux上运行它,它包含了所需的一切。这意味着他们得经历一个烦人的构建过程,把这些模板转换成Go代码,以便将它们嵌入到二进制文件中。

Mark Bates:是啊,Buffalo也是一样。任何需要生成内容的工具都需要附带它们的模板,但这真的很麻烦……你刚才提到的保持这些Go文件同步的问题,真是令人头疼。因为要把这些文件嵌入进来,唯一的办法就是生成Go文件。所以你要么得确保这些Go文件始终保持同步---就像你说的那样,保存在磁盘上---这样如果有人执行go get,他们就会得到嵌入的文件……要么你得设定好预期,如果你想要嵌入的文件,你得运行特定的构建脚本来生成这些文件。而这也是个很麻烦的事情……而在这个新系统中,你不需要这样做。它就像Go模块一样,流程更加简化……

Mat Ryer:Wayne,你在//go:embed之前用过其他解决方案来解决这个问题吗?

Wayne Ashley Berry:[笑] PackerPkger……

Mat Ryer:从没听说过这些……听起来不咋地。它们是什么?

Wayne Ashley Berry:我想这通话里有人应该听说过……

Mark Bates:是啊,我想维护者肯定很高兴1.16发布了,他再也不用维护这些工具了……

Mat Ryer:不过这是真的,因为现在你确实不需要再构建这些工具了,对吧,Mark?

Mark Bates:是啊,不只是我。正如我所说,这是一个社区多年来都在面对的问题。Go-bindata、Static、Gorice……这些工具的名单可以列得很长。而且我们所知道的只是其中一部分---这些年还有很多类似工具……我敢肯定很多人都写过自己的工具。

Mat Ryer:我肯定写过。我有时候只需要一个HTML文件,我一开始就是在Go代码里创建一个常量。但这样你在IDE里得不到任何帮助,所以后来我有了单独的HTML文件,然后写了个小构建脚本---我不能直接运行go build,得先运行一些其他的东西。这个脚本基本上就是做Hugo那样的事情。但有了//go:embed,你就可以直接用正常的构建工具链了,对吧?

Carl Johnson:是的。有句建筑学上的谚语说,你应该“铺好草地的小路”---我不确定更准确的说法是什么……但意思是,如果你有个校园,比如大学校园,里面有不同的建筑,你在考虑“该在哪铺设人行道呢?”一种方法是你猜测,“我觉得人们会经常从1号楼走到3号楼,所以我们在这里建条路。”但另一种方法是,你先铺一片大草坪,留它一年,然后回来看看,“哦,人们老是从3号楼走到4号楼,草已经被踩平了。那我们就在这里铺人行道。”

我觉得Go团队在铺设草地小路方面做得非常好……我不知道该怎么说,但他们会关注开发者们真正遇到的问题,然后尝试让Go工具能够解决这些问题。

我们之前提到的模块系统,它解决了人们在处理开源软件时遇到的实际问题,比如“我们如何将开源软件整合到我们的程序中?如何知道我们引入的是什么版本,输出的是什么版本?”等等……

这是另一个类似的例子,多年来人们一直在开发各种工具,包括Packer、Go-bindata、Static等,名单还可以继续列下去,现在我们终于在Go工具中内置了这个功能。所以对于我们这些一直在使用这些工具的人来说,看到它被内置进来真的非常令人兴奋。

Wayne Ashley Berry:是的,这让我想起我刚开始使用Go的时候……你会觉得Go承诺提供一个完整的工具链。它有内置的编译命令、测试命令……你会得到一个单独的静态二进制文件……我甚至曾经通过Slack发过一些二进制文件给别人……这真的很有用。

Mat Ryer:嗯。与其通过邮件,你可以把消息嵌入到二进制文件里,仅仅是为了使用这个功能…… [笑声] 这是个不错的主意。

Wayne Ashley Berry:你开始使用Go,然后你发现“哦,我其实还需要这些HTML文件,我还需要这些CSS文件”,你就开始失去那个简单的部署机制。而现在我们又回到了那个状态,你不需要再费心去找“我需要用哪个工具来嵌入文件?”每个人都可以使用同样的工具,各个项目之间也可以有统一的标准,这是真正实现了Go最初的承诺---每个人都有一套相同的工具,它们都能正常工作。

Mat Ryer:太棒了。那关于秘密信息呢?你应该用//go:embed来把秘密信息嵌入到二进制文件中吗?还是说你必须假设别人依然能看到这些内容?

Mark Bates:我觉得你总得假设别人能看到一切。 [笑声]

Carl Johnson:是啊,如果你只是想把它发到服务器上,我觉得只要你保密好二进制文件,那应该没问题。但如果你是发给客户的,客户很容易反编译二进制文件,提取出秘密信息,然后把它传到暗网上。所以这大概不是一个好的用例。这还是取决于秘密信息的性质。

Mark Bates:是的。不过有一个用例你可以考虑嵌入一些类似秘密的信息,比如某个应用里为特定客户内置了许可证……最糟糕的情况是他们反编译了许可证文件并提取了它,但他们的应用程序可能还会与许可证服务器进行交互,所以如果他们篡改了它,可能会导致他们的二进制文件无法正常工作。

Carl Johnson:是的。

Mark Bates:这就是其中的一种方式……另外,你还可以使用Go标签来基于客户的许可证模型构建二进制文件,对吗?

Carl Johnson:是的。我遇到过一个类似的问题,就是如何在二进制文件中包含构建版本信息……有几种不同的方法可以做到这一点。一种方法是使用Go链接器,如果你给它发送特定的命令,你可以说“这里有一个字符串变量,请用这个替换它”。这样你可以写一个小脚本,在构建二进制文件时,用你想要的git哈希值替换掉版本字符串。问题是,现在你非常依赖这个脚本来构建项目,否则你就会得到一个空字符串……另一种方式是使用Go embed,你可以把Git哈希值写到一个简单的文本文件中,比如叫version.text,它可以是一个人类可读的版本号,比如1.2.3,或者是一个git哈希值,或者是你需要的任何东西。然后你可以在打包时将它嵌入到二进制文件中。如果客户说“这对我不起作用”,你可以说“那你运行一下command -v,告诉我你用的是什么版本,然后我就能告诉你问题出在哪里了。”

Mat Ryer:所以//go:embed的工作原理是将文件嵌入到二进制文件中……不过你不能通过go generate来运行可执行文件,也不能运行脚本,或者其他东西。

Mark Bates:对。不过正如Carl刚才所说,//go:embed有一个特点---我们还没有深入讨论它是如何工作的,但它基本上有两个概念……你可以嵌入一个文件系统,就像你想象的那样,它是一个文件的集合;你也可以直接将内容嵌入到一个字符串或者字节切片中。

Mat Ryer:哦……这很有趣。

Mark Bates:所以以Carl的例子为例,你可以像现在一样有一个版本字符串……但是你可以使用//go:embed直接将版本号嵌入到那个字符串或字节切片中。

Mat Ryer:是啊,不过你可能仍然需要事先运行脚本来准备好那个文件,但至少你不再需要处理那些麻烦的标记,比如你之前提到的linter标记或者链接器标记。

Wayne Ashley Berry:是的,另外一个好处是,如果你期望的文件不存在,你会得到编译错误……而不像使用ldflags或者其他一些hacky的解决方案,有时候你只会得到一个静默的错误,然后你就打包了一个没有版本信息的二进制文件。

Mat Ryer:是的,这确实很好。有人应该写一篇关于如何用现代方式解决这个问题的博客。我每次都用ldflags来做这件事。

Mark Bates:我也是这么做了很多年……

Wayne Ashley Berry:是啊,我也一样。 [笑声]

Mat Ryer:我把默认值设为def

Carl Johnson:我还得自己谷歌一下,找到我之前写的博客,看看ldflags的用法,然后复制粘贴,祈祷我写的时候是对的。

Mark Bates:哦,我觉得大家都是复制粘贴的。我们都在某个地方写过一个,然后就反复用它。

Wayne Ashley Berry:……给自己用的。

Mark Bates:然后就到处复制粘贴。

Mat Ryer:是啊。

Wayne Ashley Berry:我们甚至有一个内部命令,它会自动生成那个标记的参数……

Carl Johnson:哦,太好了。

Wayne Ashley Berry:……然后你只需要把那个命令的输出传递给go build

Mark Bates:看,这很棒……我们有这么多老的、hacky的解决方案,现在终于可以开始摆脱它们了。

Mat Ryer:好吧,也许我们可以更深入地探讨一下//go:embed是如何工作的……它其实是一种特殊的注释,对吧?这在Go里是很少见的设计之一,我觉得这是Go设计中的一个不寻常之处,特定的注释有特殊的含义。Go generate是一个例子,还有构建标签,但它实际是怎么工作的呢?如果你想把一个文件嵌入到一个字符串中,你该怎么用//go:embed呢?

Mark Bates:其实它的使用非常愉快,也很简单,操作相当直接。当然,我很犹豫---我从不想说“简单”或者“容易”,因为它从来都不是那样的……事实上,我曾经在尝试按扩展名嵌入文件时遇到过困难,[听不清,00:20:05.22]。基本上,你要做的是设置一个变量,无论是字符串、字节切片,还是一个embed.FS变量。这基本上就是你的三个选择。如果我漏了什么,请有人补充,但我几乎可以肯定这就是三种选择,你可以在它们上面放置这个指令。

你写上你的go:embed指令,然后告诉它你想要哪些文件。而且,作为一个写过这些系统的人,这让我很开心……你请求的文件是相对于源代码的。这就带来了一种一致性。如果我在cmd/foo/main.go里引用了templates/css,它会期望templates文件夹就在main.go旁边,诸如此类。而如果你不在Go工具链里做这些事情,比如你要在市场之后做这些操作,这种解析问题就会非常棘手。

它适用于我之前提到的三种情况。你可以说“我想要模板”,所以你可以用template/*,这是一个通配符……你还可以用*.css,例如。我遇到的困难是我有assets/css/,里面有一堆CSS文件,然后我在我的embed指令里写了assets/*.css。这样它只会查看一个目录,所以我需要另一个星号,再加一个斜杠,来递归地遍历所有文件。但一旦我搞明白了,就很好用了。

Carl Johnson:这里有个坑,就是Go内置的模式匹配---它在filepath.match里实现的---其实有点糟糕,老实说……它对它的用途来说还可以,但它故意设计得非常简单,就像很多Go工具一样……它不支持**。如果你熟悉很多JavaScript的资源构建工具,它们都有**/*.css,这意味着在某条特定路径下的任何位置的CSS文件。而Go的文件路径匹配器不支持这个。它只支持在特定位置使用单个星号。所以如果你写go:embed assets/*.css,它会找到你在assets文件夹下的所有CSS文件,但不会找到assets下的css子文件夹里的文件。所以这点要小心。

Mat Ryer:是的,这很有趣。不过我不太介意……

Mark Bates:嗯,就像你说的,你可以用一个中间的星号,只用一个星号,来匹配所有文件夹……

Mat Ryer:不管它们有多深吗?

Mark Bates:我不确定……

Mat Ryer:不过话说回来,从某种程度上讲,清晰明确反而更好。如果你想嵌入更多内容,你可以用不同的……你懂我的意思吗?如果你有很多CSS文件,而且它们的路径很复杂,甚至有些CSS文件名字相同,路径不同---这种情况在有主题化需求时经常发生---那可能会很难找到你想要的文件。这很棘手。

Mark Bates:是啊。我遇到的问题是我有嵌套的JS文件,还有一个vendor目录,想在vendor目录中找到文件,这让我遇到了问题。不过它确实非常简单……就像我说的,它非常基础,非常简单。

让我惊讶的是---我用的是Neovim加vim-go,当我的模式有问题,或者文件不存在时,我会在编辑器里收到go vet警告。

Wayne Ashley Berry:哦,真不错。

Carl Johnson:这很有趣,确实。

Mark Bates:所以就在我的编辑器里,我能看到一个漂亮的小警告,告诉我“哦,这个模式不对。”

Mat Ryer:这很不错。而且这应该也是个构建错误,对吧?

Mark Bates:是的,我相信是。

Mat Ryer:是啊,确实不错。

Wayne Ashley Berry:其实,我忘了你也可以指定多个目录和多个模式,如果你是把内容嵌入到一个文件系统里的话……我一开始的做法是,我会有一个var css,然后把CSS目录嵌入进去;然后再有一个var images,把图片目录嵌入进去。但其实你可以只用一个var static,把所有内容都嵌入进去……你只需要记住它们仍然存在于它们各自的目录里。所以你需要引用html/index.html

Carl Johnson:是的。这其实是一个非常好的做法。所以如果你有一个var static,或者var fs,然后你在它上面的go embed注释里写上go:embed assets/css/*.css assets/js/*.js,然后再加上图片文件,等等……你可以把所有内容都嵌入到一个文件系统中。

Mat Ryer:这很酷。

Mark Bates:是啊。而且如果那行代码太长,分隔符就是空格,你可以换行。

Mat Ryer:哦,真的吗?

Wayne Ashley Berry:我不知道这点。

Mark Bates: 你可以在变量声明上方使用多个 go embed 指令。这样你可以把两三个东西很好地嵌入到一行中,但如果有更多元素,你可以做一个有序的列表,甚至可以对它们进行排序,让一切看起来非常整洁。所以这也是非常好的方式。你可以通过这种方式非常有条理地构建你的静态资源。

Carl Johnson: 如果你赶时间的话,只需要简单地使用 go embed assets,它几乎会递归嵌入 assets 文件夹中的所有东西。它不会嵌入的文件是以点开头的文件(dot files),因为这很符合预期。它也不会嵌入以下划线开头的文件……背后的逻辑是 Go 不会编译以下划线开头的文件。所以,如果你有 _myfile.go 文件,Go 编译器会忽略它。老实说,我觉得这个逻辑有点奇怪……但如果你觉得这是个问题,你可以显式地命名你的下划线文件,或者使用 assets/_* 来绕过这个问题。

Mark Bates: 是的,在 Ruby on Rails 中使用下划线文件名这个模式很常见,通常用于部分模板(partials)……所以任何从那个世界带过来这种理论的人……这确实是一个很好的陷阱。我以前没有意识到它会这样,像丢掉一个以下划线开头的 HTML 文件之类的情况。

Carl Johnson: 如果你使用 template/*.html,它会包含下划线文件。

Mark Bates: 是的,这是默认行为。

Carl Johnson: 如果你只是说嵌入 templates,你可能想“好吧,它会嵌入 templates 文件夹以及所有子目录中的所有东西。”幸运的是,当你尝试使用某个部分模板时,如果它不存在,这种错误你会立刻注意到。

Mark Bates: 是啊。这其实是一个很好的引子,接下来是关于工具的讨论。Go 工具会告诉你它期望在代码中嵌入什么文件。所以如果你运行 go list -json,它会输出一个 JSON 格式的构建信息---基本的模块和包信息,其中包括它将要嵌入的所有文件。

Wayne Ashley Berry: Pkger 里有个类似的命令,不是吗?我经常使用那个命令,因为有时候文件会莫名其妙地消失,而在持续集成(CI)中你需要查看实际发生了什么。

Mark Bates: 是的,这是一个非常好的方式……显然,如果你愿意,你可以用它进行测试……但这就意味着你开始测试语言本身了,我觉得……不过工具确实提供了这些信息。所以如果你疑惑“到底发生了什么?实际嵌入了哪些文件?”,你不需要去翻查调试日志,只需要快速运行 go list -json。你会看到“这就是六个嵌入的文件。我以为应该嵌入 106 个文件。我的模式错了,或者我遗漏了整个文件夹。”这样可以帮助你立即回到问题的根源。

Wayne Ashley Berry: 这很有趣,Go 通常是一个非常简单的语言,几乎没有什么“魔法”,但有时候你会发现一些嵌入了某种观点的设计……比如自动排除以下划线开头的文件。如果你不知道这个规则,就会感觉不太直观,甚至觉得它有点像框架而不是语言了,因为 Go 的作者加入了他们的设计理念……通常,我发现最好是顺应这些设计,享受它们,它确实让一切变得简单明了。但你确实需要弄清楚这些设计理念是什么。

Carl Johnson: 你说的很有道理。

Mat Ryer: 是的,确实很礼貌,不是吗?[笑声] 说到这里---虽然我们还没有进入“不受欢迎的观点”环节,但如果你觉得 Go 的魔法注释有点奇怪,那 import _embed 又是怎么回事?你必须导入 _embed

Carl Johnson: 其背后的逻辑是,他们不希望使用 Go 1.15 或更低版本的人在构建需要嵌入的东西时,误以为它可以正常工作,直到你运行它时才发现其实并不能工作。为了避免这种情况,每次你使用 embed,都要导入 embed 包。但如果你只是将文件嵌入为字符串或字节切片,你实际上并不需要使用 embed 包。所以为了绕过这个问题,你需要 import _embed,这告诉编译器“好吧,我在这个文件中使用了 embed 功能,确保它可用。”但如果你不明白它的存在理由,它看起来就很奇怪,像是“我得包含这个没有实际作用的导入,没什么道理。”

Mat Ryer: 是啊。

Carl Johnson: 但它确实有道理。

Mark Bates: 嗯,我们已经在 Go 的好几个地方这么做了;数据库包的注册就是一个很好的例子,它们通过副作用来实现。现在,我们可以讨论它们是否应该通过副作用来实现……

Mat Ryer: 我们不能讨论,它们不应该。

Mark Bates: 我有我的看法……但通过副作用,它会被注册到一个全局映射中。因此,标准库中有这种技术的先例。这并不意味着我喜欢它……

Carl Johnson: 不过这比那个更严格,因为在数据库的例子中,你只需要在 main 包中导入它。或者在你的整个程序中只在一个地方导入它。

Mark Bates: 是的。

Carl Johnson: 但在这里,每次你嵌入一个字符串或字节切片时,你都必须确保导入了 embed,否则它会提示“你没有导入 embed。”

Mat Ryer: 我喜欢这样。

Mark Bates: 我倒不太介意……

Mat Ryer: 是的,我也挺喜欢的。

Mark Bates: 作为一个也写过类似工具的人,我把它看作是一个标记……在我开始解析整个 Go 文件之前,他们有没有使用这个包?如果他们没有使用 embed,我为什么要解析这个?所以对我来说,这比注册数据库驱动的副作用要不那么令人反感一些。

Wayne Ashley Berry: 我通常会把所有嵌入的资源放在一个文件里。所以顶层有一个 resources 目录,Resources.go 是唯一会嵌入资源的地方……然后其他包只需要从那儿导入它们,不需要知道任何关于嵌入的东西。不过这是一个我希望工具能够改进的地方……比如,如果 VS Code 或者 Vim、Neovim 能检测到你使用的是 Go 1.16,并且代码中有 //go:embed 指令,它可以自动为你导入,而不是不导入。

Mat Ryer: 我觉得它会这么做。

Carl Johnson: 我相信 goimports 最终会更新这个功能,如果还没有的话。

Mat Ryer: 是的,我也这么认为。我曾经向 Brad Fitzpatrick 提过这个建议,就是是否应该仅仅通过导入一个包来依赖它的副作用---是否在回顾时应该改变这一点……他的表情告诉我“是的,你是最棒的”,所以……而那是 Brad Fitzpatrick。

Mark Bates: 如果我没记错的话,那天你可能只是吃了糟糕的午餐。我们那天去了一个很可疑的地方……

Mat Ryer: 不,那挺好吃的……

Mark Bates: 嗯,好吧……

Mat Ryer: [笑]

Mat Ryer: 我有个小问题,想问你们仨……你嵌入过的最棒的东西是什么?Mark,你先回答……别对着麦克风外笑。我们需要那个笑声。我真的需要。

Mark Bates: 我嵌入过的最棒的东西是一幅 Jim Wyrick 的 ASCII 图片。

Mat Ryer: 哇,这个回答太棒了。

Mark Bates: 谢谢。

Mat Ryer: 有谁能比这个回答更好吗?

Wayne Ashley Berry: 我有个类似的回答,但我在测试中嵌入了一张皮卡丘的图片,因为我们有一个算法在检测图像中的显著颜色。所以我在测试中嵌入了皮卡丘,这样我就可以通过代码运行它。

Mat Ryer: 这确实是个好答案。

Carl Johnson: Mat,你一开始说我有哲学博士学位……这在我的工作中很少有用武之地,但这次有用,因为我嵌入了一个 quine。Quine 是计算机科学中的一个笑话,名字来源于 W.V.O. Quine,他是 60 年代非常活跃的哲学家。它在计算机科学中指的是一个可以打印出自身的程序。

所以如果你想写一个可以打印出自己的程序,只需要使用 go embed,嵌入你的文件,比如 file.go,然后打印出这个嵌入内容。它是递归的,嵌入了它自己。

Mat Ryer: 这太棒了。这非常“元”,让我觉得这就是《终结者》故事的开端。大概就是这么开始的……

Carl Johnson: 是的,它嵌入了自己,然后变得越来越复杂。

Mark Bates: go:embed 就是 Skynet) 的起源。不是亚马逊的无人机,或者其他那些东西……就是这个。

Mat Ryer: 它使用 AWS 的 API 来控制那些无人机。

Mark Bates: Russ Cox 用 //go:embed 启动了 Skynet。谢谢你,Russ。

Mat Ryer: 还有 go generate

Wayne Ashley Berry: 这让我想起了……是不是 Russ Cox 在 YouTube 上做过一个关于 //go:embed 的草案设计演讲?

Mark Bates: 是的,我相信是他。

Wayne Ashley Berry: 这是去年的六月吗?他做的第一件事就是在一个函数中嵌入文件。而有趣的是,这也是我在 Go 1.16 发布后尝试做的第一件事,但实际上你不能这样做。你必须在包级别的变量中嵌入文件。老实说,我不知道该如何看待这件事……

Mat Ryer: 是的……很有趣,因为我们很多人都在努力避免使用全局状态……但在某种程度上,这打破了那个规则,这样可以吗?

Mark Bates: 在这个提案的最早期版本中---我想甚至是在公开发布之前的那些版本---有些人传阅过这个提案,尤其是那些写过类似包的人……这是我最早的评论之一:“为什么我不能在函数级别嵌入文件?我觉得人们真的会想要这样做,这个问题一定会浮现出来。人们不喜欢全局变量……诸如此类的抱怨。”

我不太记得 Russ(Cox)的确切问题是什么,但他确实提出了一些非常合理的观点,既涉及到技术问题,也涉及到为什么在实践中你可能会真的希望这种功能是全局池的一部分……就像你的文件系统。文件系统是一个全局可访问的池。你不可能只在某个函数中有一个独立的池,这在概念上都说不通。所以他对这个问题有很多有趣的见解。

Carl Johnson: 如果你拿到最早的 Go 1.16 beta 版本,它实际上允许你在函数级别嵌入文件……但后来人们在使用时发现了一个问题:如果你嵌入了一个字节切片,有人可能会修改这个字节切片。然而,这种行为该如何处理并不明确。比如说,我有一个函数,它嵌入了一个文件作为字节切片,然后有人修改了这个文件...... 那该怎么办?这种行为的语义是什么?应该导致崩溃吗?还是应该合法?在重新运行这个函数时,应该返回一个新的副本,还是应该返回刚刚被修改的那个?

这个问题太复杂了,所以他们决定“让我们只允许在顶层嵌入文件”。从逻辑上讲,嵌入文件确实只有在顶层才有意义。因此,他们最终放弃了在函数级别嵌入文件的功能。虽然这有点不方便,但这也回到了 Go 的设计理念---Go 的作者有非常强烈的观点。Go 不是个神奇的语言,它很简单,但它的设计理念非常明确。如果你真的需要全局变量,你可以处理它,只要不要用错。

Mat Ryer: 但你仍然可以修改它们,对吧?它们是全局变量。

Mark Bates: 是的,在嵌入文件系统(FS)的情况下,你可以替换一个文件系统(FS)为另一个文件系统。但文件系统本身是只读的。它们是线程安全的,设计上是供全局使用的,而且是只读的。你不能修改它们,除非你替换整个文件系统。

Mat Ryer: 明白了。

Carl Johnson: Mat,我觉得你在说的问题是,如果你有一个字节切片,它在顶层,你当然可以修改它。但如果它在一个函数中,你可以修改它,然后你会重新运行那个函数。那这时候你应该得到一个新的原始副本,还是应该返回刚刚被修改的那个嵌入的副本?如果你熟悉 C 或 C 系列语言,它们有一个静态变量的概念,每次你运行时都是同一个变量……

Mat Ryer: 是的,第一个变量。

Carl Johnson: ……如果你修改了它,那这个变量在不同运行之间是相同的……但在 Go 中没有这种概念,所以他们得基本上为此发明一个新概念才行。

Mat Ryer: 是的,这确实有道理。我有点赞同你的观点。我不介意这些限制,这实际上是学习正确做事的方法。你总是可以传递参数,你也可以将一个全局变量传递给某种类型或其他东西,如果你想这么做的话……

Mark Bates: 顺便说一下,这也是 Go 鼓励的方式。你被鼓励写一个函数接收 fs.FS 接口。

Mat Ryer: 对,这样你就可以很轻松地进行测试。

Mark Bates: 是的,这样你就可以进行测试。比如你可能有一个全局的 CSS 文件夹,但你的函数只接收 fs.FS 类型。所以你可以传递那个 CSS 文件夹,或者你可以使用测试包中的 mapFS,它用于创建你自己的虚拟文件系统进行测试,你只需要把它传递进去。你也可以自己为所有这些写接口,然后实现这些接口,在中间做各种有趣的事情……但你被鼓励让你的函数接受 fs.FS 作为参数,而不是直接引用全局变量。这个方法也是他们绕过这个问题的一部分。

Carl Johnson: Mark 说的是有两种不同的类型。一个是 embed.FS,它专门用于嵌入这些文件组,还有一个新类型叫做 io/fs.FS,这是一个接口,它允许多种不同的类型实现文件系统。所以 embed.FS 实现了这个接口,但 ZipReader 也实现了这个接口,memFS 也实现了这个接口,他们还在为 tar 格式做一个 fs.FS 实现……如果你曾在 Go Playground 上使用过,你可能知道它可以包含多个文件,那种格式叫做 tex tar。他们正在为这种格式实现一个 fs.FS 实现。

所以,只要你有一组文件的格式,你都可以实现 fs.FS。如果你的函数或方法接收一个 fs.FS,你就可以传递给它。不一定是嵌入到二进制中的 embed.FS,它可以是你在运行时随时替换的任何东西。

Mark Bates: 包括本地文件系统。

Carl Johnson: 是的,包括本地文件系统。

Mark Bates:os 包中有一个助手函数(我相信是在 os 包中)可以让你获取底层的操作系统。如果你在构建一个工具,它需要查看底层的操作系统并且你接收的是一个 fs.FS,你可以直接获取并传递它。

Mat Ryer: 那这就引出了一个问题---你觉得如果要处理本地文件系统的文件,使用 FS 会是最佳实践吗?我们应该使用这种抽象,因为它更加通用?还是你仍然会直接使用 os.Open

Mark Bates: 老实说,我打算使用它,因为它确实让我的测试变得更简单……我写了很多处理文件系统的工具,从生成器、转换器,到读取和写入……能够模拟文件系统是非常棒的。

Wayne Ashley Berry: 这个接口也是只读的吗,就像嵌入的文件系统一样?

Mark Bates: 是的。

Carl Johnson: 目前是只读的。

Mark Bates: 是的。你不能往里面添加文件,之类的。但正如我所说,FS 测试包里有一个 mapFS,你可以用于测试……所以你可以定义所有你想要的不同文件。

Mat Ryer: 既然这些操作是在构建时发生的,那有没有一种方式可以让你编辑 CSS 文件并实时刷新,从而查看更新?还是你每次都得重新构建?你知道我在说什么吗?有没有一种被动模式,它可以自动读取文件?还是你得自己在特定的情况下构建这种功能?

Carl Johnson: 这就是 fs.FS 的作用。你可以在程序中,根据你如何处理命令行参数、标志或变量等,来设置“如果这个值为 true,则使用 embed.FS,如果为 false,则使用 os.FS,并根据需要在两者之间进行切换。”所以这可以是像 Buffalo 这样框架开发中的一种非常好的方式,它可以在文件在磁盘上被修改时自动刷新。但当你需要构建并将其发送到服务器或用户端时,你可以将文件嵌入到二进制中,确保它是固定的。

Wayne Ashley Berry: 其实我喜欢这种行为不是默认的,因为我不经常做这种工作,而我发现现有的第三方工具总是在做这些事情时遇到很多问题。比如嵌入的本地文件系统、生成的代码等,我很难搞清楚到底在读取哪些文件。我还是更喜欢只有一种方式来做事,不管是在本地运行还是在部署时,文件总是以同样的方式嵌入。所以我真的很喜欢这种方式,但那个接口也非常棒,因为我现在可以遍历我们所有的库,包括标准库……它可以变成一个通用的抽象点。

Mat Ryer: 是的。

Mark Bates: 它已经出现在很多标准库的地方了。Carl 刚才提到了很多例子,比如 http 包支持 fs……

Mat Ryer: 哦……

Mark Bates: ……用于提供静态文件。模板包也支持 fs。所以对于那些写代码生成器的人来说,能够解析一个 fs 是非常好的。还有很多这样的好处。

Carl Johnson: 你甚至可以以一些有趣的方式来组合它们。比如,你可以向客户端分发一个 zip 文件,然后因为 zip 文件现在可以当作一个 fs,你可以将 zip 文件变成一个模板文件系统。所以与其告诉用户“这是一个模板目录,你需要解压并放在某个特定位置”,不如直接给他们发送包含所有模板的一个文件,他们只需要指向那个文件,剩下的事情都会自动完成。

Mat Ryer: 嗯……这确实不错,不是吗?

Mark Bates: [笑]

Mat Ryer: 这真的不错。

Mark Bates: 终于有了一些关于文件的接口……

Mat Ryer: 是啊。

Mark Bates: ……太好了。就像你说的,我不知道其他 Go 开发者怎么样,但我经常和文件系统打交道,我也经常尝试把文件通过流水线处理。首先,我想把 Markdown 转换成 HTML,然后我想通过 Go 模板处理它。你知道,所有这些不同的事情,能够有接口支持;只需要不断修改并传递文件的不同版本,这真的太棒了。

Mat Ryer: 对。我期待云服务商也在他们的客户端中实现它,这样你就可以直接使用 S3 存储桶中的文件系统,或者其他可用的存储。

Mark Bates: 这也是另一个有趣的地方,现在你可以为 S3 写接口,它看起来就像普通的文件……你可以写一个文件系统接口来与 S3 通信。或者与数据库通信。所以你现在可以把 Postgres 当作一个虚拟文件系统。如果你愿意,你可以做所有这些不同的事情。你可以像你说的那样,把 S3 用作一个虚拟的只读文件系统……或者如果你做的是嵌入式开发,你可以使用 SQLite。

Carl Johnson: Go 的一个很棒的特性一直是 io 包。当你是一个新手 Gopher 时,这可能有点让人困惑……比如“什么是 io 包?这些 readwrite 方法是什么?为什么我必须使用它们?为什么不能直接用字符串?”但当你理解了它们的工作原理后,你会发现 io.Reader 基本上是一个只读的文件,io.Writer 是一个只写的文件……它让你可以抽象出文件的具体存储位置。这个文件是在磁盘上吗?它是你正在读取的 HTTP 响应吗?它是在某个 S3 存储桶中的吗?

Go 一直以来都有一种抽象单个文件的方式,那就是通过 io 包。但是现在通过 io/fs 包,你可以抽象整个文件系统。所以你不仅仅是在看一个文件……因为你总是可以说“我从 S3 获取了一个 io.Reader”,或者“我从一个 zip 文件中获取了一个 io.Reader”等等。但现在你可以处理整个系统。

Mark Bates: 是的,但 io.Reader 没有文件大小、修改时间,也没有其他这些属性。

Carl Johnson: 对,它们没有作为磁盘文件的那些属性。它们没有文件名,也没有权限。

Mark Bates: 完全正确。

Carl Johnson:所以现在我们可以模拟所有这些东西了。

Mark Bates:是的,这让我感到非常兴奋。但我喜欢用代码做一些可怕、糟糕的事情。

Mat Ryer:我见过一些,挺好的。接下来还会抽象什么呢?所有的东西。其实如果看看时间,该到了我们“不受欢迎的观点”环节了。

Jingle:[49:33] 到 [49:50]

Mat Ryer:好了,今天谁有不受欢迎的观点呢?Carl,你有什么想法?

Carl Johnson:这其实不是关于 Go 的观点,更像是关于全球开源软件的看法。我认为应该有某种政府资助开源软件的系统。想想科学,在美国我们有国家科学基金会(National Science Foundation),有国家卫生研究院(National Institutes of Health)。艺术方面我们有国家艺术基金会(National Endowment of the Arts)和公共广播公司(Corporation for Public Broadcasting)。我们有这些不同的资助来源……但在开源软件方面,目前基本上只有两种方式。一个是 Go 的做法,即有企业赞助商,比如 Google 投入大量资金和时间来开发这些功能。

比如 Go 的嵌入功能---Russ Cox 完成了大部分实际开发工作。想想看他的时间成本,这个功能可能花费了 Google 大约 1 万到 5 万美元,仅仅是工程师们花在这个功能上的时间……这还不包括所有在问题上做出贡献的人的时间。如果把所有的时间都加起来,成本会更高。

另一种资助软件的方式是通过 Patreon 模式(粉丝资助)。有些项目就是这样资助的,比如 Zig 编程语言,有人会在 Twitch 上直播或者做一些让人们感兴趣的事情,然后你付钱给他们,让他们继续作为个人开发者进行开发。但目前并没有真正的政府资助开源软件的方式,我认为这实际上会很有帮助。

我听到的反对意见是:“你是说政府应该给 Leftpad 的开发者付钱吗?” [笑声] 我认为这是个合理的批评,但我不认为这种情况真的会发生……因为如果你看看科学资助,通常政府会提出一些资助项目,资助项目可能会说“你能研究如何治愈冠状病毒吗?”然后你去那个资助委员会,说“我有这些科学家在我的团队中,我们有关于如何开发疫苗的理论。我们过去做过这些疫苗,证明我们有能力做到这一点。”然后委员会会评估你的资助提案,给出评分,得分最高的提案会获得资金。

所以在这种情况下,可能会有一个软件资助委员会,人们会看“Go 是一种受欢迎的编程语言,它有数百万开发者,他们都说很期待使用这个嵌入功能……那么为什么不给这个开发者 1 万到 5 万美元的资助,让他/她可以工作几个月开发这个功能,从而让所有人都受益呢?”

我认为这样的事情---我不认为会很快发生。这也是为什么我把它放在“不受欢迎的观点”环节……似乎大家都想削减政府资金,而不是增加政府资助……但我认为这种方式会非常有帮助,能够为开源软件的资助提供第三条途径,避免开发者的倦怠,或者避免公司改变主意,不再支持某个项目。

Mat Ryer:嗯,非常有趣。Slack 上的 Cory 提到,即使是政府系统本身也在使用大量的开源软件……所以他们本身也会直接受益。

Carl Johnson:是的。我以前和这个节目的前嘉宾 Paul Smith 在 Ad Hoc 团队一起工作……他们很棒,尽可能多地使用开源软件。只要能得到政府的许可,他们基本上都会公开源代码……但我认为这只是一个角度,即当政府开发自己的软件时,如果没有理由保密,他们应该将其开源。还有另一个角度,即对于那些对政府没用的软件,也应该有一种方式让开源维护者能够靠它谋生。

Mat Ryer:嗯,非常有趣。我们会在 Twitter 上发起投票,Go Time 的民主方式,我们会看看这到底是受欢迎的还是不受欢迎的……但这是个不错的观点。

Wayne Ashley Berry:我有一个可能不受欢迎的观点……

Mat Ryer:好,[无法听清 00:54:21.20]

Wayne Ashley Berry:……我的观点是我们应该尽量减少使用模拟(mocks),而且使用的模拟数量应该随着时间的推移而减少。这不是 Go 特有的,只是编程中的普遍原则……我实际上采用了一个我从一个乐队成员那里听到的理念。他说:“你应该像演出时那样练习。”所以如果你在家练习时戴着耳机,音箱的音量调到 2%,然后你被期望走上舞台,把音箱开到 110%,并且期望这些技能转移---这种情况不会发生。

我认为在软件开发中,如果你要让代码在 MySQL 上运行,那就让你的代码在 MySQL 上测试。显然有一些限制……比如涉及账单时,你不想开始给自己的信用卡收费,诸如此类的情况……但通常这些服务会提供一些本地运行的模拟器,诸如此类的东西……我发现这对我来说非常有帮助。因为我已经到了这样的程度---过去一年里,我工作的某个服务实际上我从来没有在本地运行过。我只运行过测试。所以如果有人问我“运行 main.go 需要设置哪些环境变量?”我会说“我不知道。你去写个测试,运行测试,这就是你知道它会正常工作的方式。”

Mat Ryer:是的……这很有趣。我喜欢这个观点。我们也会测试这个观点。

Mark Bates:我从来不模拟我的数据库调用。

Mat Ryer:对。你总是使用真实的数据库。

Mark Bates:是的,没错。

Carl Johnson: 那你怎么看 fs.fs?它算是模拟吗,还是说它只是一个接口?如果在生产环境中你使用的是 embed.fs,但在开发中你使用的是 os.fs`---`你认为这是模拟,还是说这是不一样的东西?

Mark Bates:这是接口的实现。我能理解你的意思……

Carl Johnson:什么是模拟呢?这就是 Mat 整集节目里对 Mark 做的事……

Mat Ryer:对。反之亦然。

Mark Bates:哦,我有一个不受欢迎的观点……

Mat Ryer:好。在你说之前,我想说---Roberto Clapis 提到了一个观点,回应了 Wayne 的观点……如果你的代码使用了随机数,那么你的测试也应该使用随机数。我们通常会想要在测试代码中控制随机数种子,以便让测试结果是可预测的……但从某种程度上,这让它不再是真实环境中的情况了,实际上如果你使用随机数,可能会更好。所以这是一个有趣的观点,延伸了 Wayne 的观点……是的。如果你对此没有什么要说的,那我们就听 Mark 的不受欢迎的观点吧。 [笑声]

Mark Bates:我的观点是在我们谈论三明治的时候突然想到的……我知道,对吧?我其实并不特别喜欢培根。

Mat Ryer:哦……

Carl Johnson:哇……你被踢出互联网了。

Mark Bates:老实说,我觉得它被过分吹捧了……

Mat Ryer:嗯。

Wayne Ashley Berry:非常不受欢迎。

Mark Bates:这是一个非常不受欢迎的观点,我明白这一点。

Wayne Ashley Berry:那你更愿意吃什么呢?

Mark Bates:我更愿意吃香肠,而不是培根配鸡蛋。

Mat Ryer:好吧,这也说得通。毕竟培根有不同种类,不是吗?因为在英国,培根和我在美国吃到的非常不同。

Mark Bates:确实是,但我对两种都不感兴趣。

Mat Ryer:好吧,合理。

Mark Bates:我就是不喜欢任何种类的培根。

Mat Ryer:如果你喜欢非常软的培根,那你应该去伦敦,因为我们这里有全世界最软的培根……如果软培根是你的菜的话。其实,我有一个美国朋友点了一杯鸡尾酒,我猜在纽约这很正常……他们要求在鸡尾酒里加培根。想象一下,在纽约,一个时髦的地方,放上像美国那样坚硬的培根,直挺挺地立在杯子里……你懂的。但如果是软的培根,那就不能加进饮品了。说实话,这太恐怖了,简直让我做噩梦。他们最后没有这么做,但……

Mark Bates:是的,这种做法没什么吸引力。不过,嘿,这就是我的不受欢迎的观点。

Mat Ryer:我们看看这个观点有多不受欢迎吧。可能对素食主义者来说不那么不受欢迎……但对食肉的听众来说可能就不太好了。

Mark Bates:是啊,我想也是。不过我确实喜欢其他很多种肉类……

Mat Ryer:嗯,你知道的,也许我们该把这个留到下一期节目,因为 [无法听清 00:58:43.19]。

Mark Bates:我也这么想……下次我们可以聊聊 Mark 最喜欢的肉类清单……

Mat Ryer:最喜欢的肉片种类……我们基本上只有薄的肉片,因为那些可以从门缝塞进来,但……一块多汁的肋眼牛排就没戏了。

Mark Bates:没错。

Mat Ryer:好吧,还有谁有什么“疯狂”的事情要补充吗?“疯狂”可能不是我该用的词。还有谁有其他古怪的事情要补充吗?没有吗?

Wayne Ashley Berry:没有。

Mat Ryer:好吧,我们的时间快到了,但我们可以留几分钟聊一些轻松的话题……Carl---

Mark Bates:没有什么比告诉大家“我们要聊轻松的话题了”更能让人感到放松的了。 [笑声]

Wayne Ashley Berry:开始吧,开始吧!

Mat Ryer:这是轻松聊天时间,Carl,你有一个博客,不是吗?我看过你在博客上写的关于如何使用 go:embed 的文章。你的博客的网址是什么?

Carl Johnson:是 blog.carlmjohnson.net。

Mat Ryer:哦,这是你的中间名吗?

Carl Johnson:哦,是的,Carl 是以 C 开头的。不过,巧合的是,m 就是我的中间名。

Mat Ryer:好名字。Wayne,你的中间名是 Ashley,对吧?

Wayne Ashley Berry:没错,确实是。

Mat Ryer:你是用 Wayne Ashley Berry 全名,还是只用 Wayne Berry?

Wayne Ashley Berry:我用全名。我不知道为什么……这是我被赋予的名字,所以为什么不用呢?

Mat Ryer:是的。我上学时有个同学叫 Ashley Berry……所以当我听到你的名字时有点触动。他是个彻头彻尾的白痴。 [笑声] 他曾试图放火烧我的裤子。

Wayne Ashley Berry: 哦,天哪……

Carl Johnson:你们三位中有人知道最有名的 Carl Johnson 吗?

Mat Ryer:他在《辛普森一家》里吗?

Carl Johnson:很接近……

Mat Ryer:那就不是了。

Carl Johnson:他是《侠盗猎车手3:圣安地列斯》里的 CJ。

Mat Ryer:那是他的全名吗?

Carl Johnson:他的名字是 Carl Johnson。所以如果你在网上搜索我的名字,没有 m,你会看到 Carl Johnson 站在车前的照片,画面非常低的多边形分辨率。

Mark Bates:有趣的是,如果你搜索 Mark Bates,你会看到一样的东西……不过是我站在车前的照片……

Carl Johnson:是的。

Mark Bates:……而且分辨率也非常低。

Carl Johnson:穿着背心,嗯……

Mark Bates:对啊……不然怎么能站在车前呢?

Carl Johnson:这还用说吗。

Mat Ryer:但我们还是这么做了……我们确实做了。好了,今天的 Go Time 节目时间到了……感谢收听,也感谢 Mark Bates,感谢你来参加节目。Carl Johnson,你得再回来一次。

Carl Johnson:谢谢。

Mat Ryer:Wayne Ashley Berry,你也要再回来一次。节目很棒,信息量很大。

Wayne Ashley Berry:谢谢你们邀请我。

Mark Bates:等等,抱歉,他们可以随时回来?

Wayne Ashley Berry:[笑]

Mat Ryer:哦,你注意到了……

Mark Bates:你刚刚好像在赶我走似的。你就像是“谢谢你来了,Mark。再见!”

Wayne Ashley Berry:我想是你对培根的评论让它变成了这样。

Mark Bates:嗯,Mat 是个素食主义者,所以他肯定会认同这个观点。

Mat Ryer:是的,我不吃培根。别告诉别人。

Carl Johnson:不过也有假培根。你喜欢假培根吗?

Mat Ryer:我不明白为什么我们要花这么多科学精力去制作假肉……所以不---嗯,我不明白。

Carl Johnson:我尝试过素食饮食,基本上我的体验就是所有的假肉都不值得……不过我觉得有些假培根还不错。

Mat Ryer:公平地说,现在有一些汉堡确实很好吃。Impossible Burger,还有另一个(我忘了名字),吃起来就和我记忆中的汉堡味道一样。而且它们对你身体真的很差,所以……额外的好处。 [笑声] [无法听清 01:02:20.01]

Mark Bates:拥有所有汉堡的健康隐患,却没有汉堡的味道。

Mat Ryer:没错。它实际上对你更不健康。比普通汉堡还不健康---不过你可以说它对动物更好……

Carl Johnson:不过它们通过每次点燃一个炼油厂来补偿这个……

Mat Ryer:确实,实际上它吃起来就像炼油厂着火的味道。

Carl Johnson:这就像碳补偿,但反过来了。 [笑声]

Mark Bates:Mat 每次吃一个汉堡都得开车 200 英里去买。

Mat Ryer:是的。这叫碳排放增量。

Carl Johnson:哦,这是碳排放增量,对。

Mark Bates:Mat 每天醒来都在想:“今天我能给宇宙带来多少碳呢?” 答案是“没有”,因为碳已经全部存在了。

Mat Ryer:嗯,是吗?不,碳是可以创造的吧……在星星内部,元素就是在那里被创造出来的……

Mark Bates:那我们打电话给 Neil deGrasse Tyson,让他下周来 Go Time 节目……

Wayne Ashley Berry:我想那是另一个播客的主题……

Mark Bates:……然后我们来解决这个问题。

Mat Ryer:他不会来的。他拒绝了,因为他是 JavaScript 爱好者……

Mark Bates:我以为是因为上次他来的时候发生了些什么。 [笑声]

Mat Ryer:是啊,我们讨论黑洞……

Mark Bates:那真是太尴尬了。

Mat Ryer:好了,如果这还不够让你困惑,那就下次再加入我们吧;我相信我们要么能保持这种水平,要么变得更糟。非常感谢……下次见。


好文收藏
38 声望6 粉丝

好文收集


« 上一篇
反射和元编程
下一篇 »
Go + Wasm