https://www.youtube.com/watch?v=rmS-oWcBZaI
今天我想分享一个关于 Go 嵌入式文件的新设计草案。
现在嵌入式文件是什么意思?
我的意思是在构建时将文件插入到您的程序中 ,
以便您可以
从您自己的程序的映像访问该文件的内容,而
不必让该文件存在于
您稍后运行的系统上。
这样做——将文件插入
到程序中——是一个非常古老的想法。
我刚刚找到了手头的几个例子 。
这是来自第六版 Unix。
这是内核 启动时运行的第一个程序。
你知道,它需要从某个地方获取一个程序 才能运行。
大多数程序都是通过分叉正在运行的程序来创建的 ,
但是第一个程序不能 从无到有,
因此这是 内核假装
位于正在运行的程序的内存空间中的第一个文件。
但这与您 从磁盘读取的格式完全相同。
它只是一个原始的可执行文件。
它恰好被写成 八进制字序列。
你知道,这就是启动的程序 。
这是嵌入程序中的文件的手写示例 。
许多年后,对于 Plan 9,我们遇到了同样的 问题 -
每个 Unix 内核都有这个问题 -
在 Plan 9 中,你知道 makefile 运行这个 代码。
它实际上构建了一个真正的可执行文件 ,而不是手写的可执行文件,
然后运行十六进制转储并将其格式化 回工作 C 代码
,并写出此文件 init.h,然后将其 包含到内核中。
如果您想在自己的系统上执行此操作,
可能会安装一个名为 xxd 的实用程序,
我猜这是一个扩展的十六进制转储实用程序。
因此,如果您将 hello world 写入 hello.txt
然后运行 xxd,它会吐出此 C 代码,
然后如果您有其他一些 C 代码 知道
此 C 代码的样子并声明 正确的
声明并使用它 ,然后你构建它,
你就有了一个 hello world。
您知道这个程序从
作为数据变量嵌入到程序中的虚拟 hello.txt 文件中读取数据 。
我们可以在 Go 中做到这一点。
今天有工具。
最古老的之一称为 go-bindata。
因此,如果我再次初始化 hello.txt
并运行 go-bindata,它实际上会生成
相当数量的 Go 代码 - 它不仅仅是一个 变量
声明 - 它有一个用于访问 这些文件的完整 API。
因此,如果我使用 MustAsset 调用编写这个 hello.go ,
它会引入命名的资产嵌入 文件 - hello.txt,
打开它,然后将其转换为字符串并 打印它,
我们就有了一个 hello world 程序。
Go 生态系统中还有许多其他工具 可以实现此目的。
这只是一个小清单。
这显然是很多人的需求 ,
不仅仅是 Go 程序员,
而且 Go 程序员肯定正在发明 自己的工具来做到这一点。
所有这些工具都存在的问题是,
您必须运行它们,然后它们写出
Go 源代码,然后必须将 Go 源代码
签入您的存储库中。
所以记住运行它们很烦人。 当您已经获得其他文件时,将
代码签入您的存储库是很烦人的
。
但这些工具对此无能为力,
因为这是 go 命令强加给它们的。
如果问题是 你
必须记住运行这些工具来生成
你必须签入的 Go 源代码,
那么显而易见的答案是为什么不让 go 命令为你做这件事呢?
go 命令可以运行工具并使用 源代码,
然后您不必签入它,也 不必
记住运行工具。
但我们不这样做。
我们提供了一种运行发电机的模式。
您可以运行 gogenerate,这将运行
源代码中提到的任何生成器,
但我们不会自动运行它。
您可能想知道为什么我们不这样做?
答案有三个。
一是我们希望保持构建简单。
接下来我们要保持构建的可重复性。
最后,我们希望保持构建的安全。
现在简单地说,我的意思是,
如果我发布一个库,我的库的客户
如果我签入这些工具生成的代码,他们就不需要我拥有的所有工具。
这极大地简化了构建 我的程序的任务。
就像我使用的 go 生成器实际上 只能在 Linux 上运行一样。
它生成完美的可移植代码,
但程序本身只能在 Linux 上运行。
那么所有的 Windows 用户都会遇到麻烦。
但如果我只检查生成的源 代码,
那么我们就不会遇到这个问题。
同样,我们希望构建是可重现的。
因此,如果该工具以某种方式表现不同,
或者用户获得了不同版本的 工具,
或者它在 Windows 上做了不同的事情,
或者它在星期二做了不同的事情,
我们只需检查 Go 源代码就可以避免所有这些问题
您应该 从该工具获取代码,而
不是每次构建时都重新运行该工具 。
最后,我们希望保持构建安全。
我们希望确保为您提供 源代码的人
不会导致在您的构建过程中运行任意代码 。
显然,如果您构建一个可执行文件, 然后运行它,
并且该可执行文件中包含其他人的源 代码,那么
您正在隐式运行他们的源代码。
你是在暗中信任他们。
但我们在构建和运行二进制文件或运行测试之间划了一条强硬的界限
。
对于构建本身,我们确实希望 保证
只有 Go 工具链和
系统上已有的工具正在运行,而
不是任何 可以将代码签
入任何依赖项的人都可以控制 列表的任意其他工具。
所有这些都是 Go 生态系统
和 Go 能够如此良好地互操作的一个重要原因,
就是我们保持下载和构建 模型非常简单,
我们希望保留这一点。
因此外部工具无法避免这个问题, 你必须运行它们
来生成必须签入的 Go 源代码 。
但是如果我们将 功能放入 go 命令本身,我们就可以避免这个问题。
对于像将 文件嵌入
到[您的程序]这样广泛需要的事情, 这样做可能是有意义的。
这就是我们在 设计草案中采用的方法。
我们有一个文件 hello.txt
然后我们有一个 Go 程序。
该 Go 程序声明了一个 embed.Files 类型的变量。
然后在它上面放置一个指令,
该指令受到 go 命令和 编译器的尊重,在编译程序时将
hello.txt 嵌入到此 embed.Files 中
。
然后当它运行时,
它可以调用greeting.ReadFile
来获取hello.txt,
然后将其打印出来。
所以这里我们有一个 hello world 程序。
因此,这个设计草案的重点 是,也许我们应该这样做,
并且我们正在尝试获得有关 您的想法的反馈。
现在,除了能够嵌入 文件并将其读回之外,
我们当然希望它能够与 Go 标准库的其余部分
以及生态系统的其余部分很好地契合。
因此,人们嵌入文件的原因之一是 通过 HTTP 提供文件服务。
另一个原因是将它们解析为模板。
或者他们可能想将它们与其他 第三方软件包一起使用。
出于所有这些原因 - 或所有这些 用例 - 我们希望确保
embed.Files 理想情况下与本机操作系统文件一样顺利工作 。
这是另一个例子。
我们已经有了 embed.Files,现在命名为 content。
我们嵌入 hello.txt。
然后我们想将其转换为处理程序, 以便我们可以提供它或将其附加到
某个 URL。
所以这里我们有处理程序,然后当 我们运行这个程序时,我们可以从这个
URL 读取,然后我们得到 hello world。
在我们进一步讨论之前,我想强调 这是一个设计草案。
这不是一个提案。
这将是我去年引入的术语的一个巨大变化 。
它是用户可见的。
它需要更改文档。
它会影响多个包。
它会影响想要使用 embed.Files 的包 。
它还会影响所有现有的工具。
希望它能简化它们。
或者也许它会让某些不再 需要。
我们希望在 提案过程的压力之外获得有关此设计草案的反馈。
因此,如果反馈是压倒性的积极,
或者如果我们必须做出改变,
然后反馈是压倒性的积极,
我们会将其添加到提案流程中 。
希望这不会引起太大争议,
因为我们已经接受了反馈并做出了 回应,
做出了我们需要做出的改变。
而且,你知道,如果一切都超级顺利,
也许我们会在 Go 1.16 中看到这一点。
或者也许我们认为这不值得做。
或者也许它会在以后的版本中发生。
我们不知道。
这仍然是一个设计草案。
这就是设计。
你基本上已经看过所有的作品了。
有一个新的 //go:embed 注释,一个指令 注释,
它使用我们已经 在
工具链中用于指令注释的语法。
有一个新的包可以作为 embed 导入, 它定义了 embed.Files。
go 命令和 编译器支持将所有这些拼接在一起,
以便在编译包时将文件复制到包二进制文件中,
然后从 那里复制
到您构建的可执行文件中。
然后,对 go/build 包
和 golang.org/x/tools/go/packages 包进行了更改,
使在 Go 程序上工作的工具
能够查看正在发生的情况,以便他们可以 更好地理解
程序的运行方式 正在构建。
你已经看到评论了。
就细节而言,
注释必须位于 embed.Files 类型的 var 声明之前 。 在这两者之间可以
有其他空白行和其他 行注释,
但否则它们需要 在文件中彼此相邻。
此注释告诉 go 命令告诉 编译器用指定的文件
预先填充 embed.Files 变量 。
//go:embed 的参数是一个 path.Match 风格的 glob 模式,并且可以
命名一个目录。
如果您命名一个目录,您将 递归地获取该目录中的所有文件和目录。
您不能引用属于其他模块的点或点-点或空 目录或子目录
,
这只是确保它能够 与模块系统很好地配合。
可以对全局变量和局部变量使用 //go:embed 注释
。
我们已经在示例中看到了其中的一个。
在测试中使用它们也很好。
他们在测试中工作得很好。
这是嵌入包。
只有一种类型:文件。
而Files有两个方法,Open和ReadFile。
Open 方法意味着 Files 实现了 新的 fs.FS 接口,
这是不同草案设计的主题 ,
我将在该视频下放置一个链接,
然后 ReadFile 方法只是一个帮助器, 使其更方便
如果您只想获取文件内容,
这是很常见的事情。
fs.FS 接口的实现 - 满足该接口 -
意味着 embed.Files 已经将 插入到期望或
了解该接口的包中。
就像 net/http 和 html/template 一样。
这就是
之前示例中的连接方式。
再举一个例子,假设您有 想要嵌入到二进制文件中的模板,
然后在程序启动时对其进行解析。
我们可以声明一个 embed.Files。
我们可以嵌入图像和模板目录中的所有内容 。
然后在运行时我们可以调用 template.ParseFS, 传入文件集,并说我们要
解析 template/*.tmpl 以获得模板。
因此它会忽略与 该全局模式不匹配的图像和其他内容。
就 go 命令的变化而言,显然 主要的变化是实际实现
我们一直在讨论的功能。
除了功能有效之外,所有这些基本上都是不可见的 。
除此之外,对于工具,我们希望 向“go list”公开更多字段。
因此,如果您运行“go list”并请求 .EmbedPatterns 或 .EmbedFiles,
它将为您提供字符串列表,这些字符串是 传递给 //go:embed
包中任何位置
或匹配的文件的模式。
因此,在此示例中,模式是 h*.txt, 然后是 images,这是一个目录。
这些文件是 hello.txt,然后是 gopher 图像。
除了 go list 的这些新更改之外,
还有其他基于 go/build 包构建的工具 -
重要的是 go 命令,
还有 x/tools/go/packages 包。
因此,在 go/build 中,其工作是真正收集 有关磁盘上源文件的即时信息,
而不是更大的上下文 - 这是 go 命令构建的基础 - 在
go/build 中,我们必须从中公开模式本身 源文件。 主源代码、测试和外部测试文件中
存在三个不同的新字段,表示 //go:embed 模式
然后你就有了 go 命令。
然后大多数工具使用的 go/packages 包会 查询 go 命令。
但它提供了更精简的 API。
它总是谈论特定的包。
即使测试包也显示为不同的 包。
因此,我们唯一需要添加的 就是
嵌入的直接文件的文件列表。
现在有很多其他设计可供您 想象。
我们已经讨论过为什么我们不在 构建期间自动运行 go generated 。
但是,如果您要进行设计, 您可能会想象添加许多其他特定于嵌入文件的内容
。
去年对第 35950 期进行了早期讨论, 探讨了
可能性的设计空间。
我想将列出的很多内容 明确标记为超出范围,因为
这里的目标是 为“这是一个文件,将其放入我的二进制文件中”提供基本支持。
对该文件的任何类型的修改都会开始 遇到,嗯,这是一种
无法内置到 go 命令中的东西,所以 它确实需要在外部工具中。
有太多的旋钮,太多你 想要做的事情,太多你想要调整的方式
。
所以我们专注于将文件放入 二进制文件中。
其他工具可以准备文件并将 其保留在磁盘上。
超出范围的内容包括数据 压缩、JavaScript 缩小、TypeScript
编译、图像大小调整、精灵图生成、 UTF-8 标准化、CR/LF 标准化。
所有这些事情都被提出来了。 您可以想象到
很多其他设计问题 ,因此,如果您对
我们为什么不这样做有疑问, 设计草案更多地讨论了其中的许多问题,
所以我建议您检查一下。
作为奖励,总结一下,这里有一个自我复制 程序。
它嵌入自己,然后将自己拉出来, 然后打印自己。
类似地,这里还有自我复制的 网络服务器。
它已经嵌入了自己,然后它就可以为 自己服务。
如果你获取 localhost:8000 并给它 文件名,它会返回自己的源
代码。
总而言之,这又是一个设计草案。
这不是一个提案。
我们首先需要每个人的反馈。
基本思想是添加直接 go 命令 支持以将文件嵌入到二进制文件中。
这在很大程度上依赖于文件系统 接口草案设计,还有
一个视频,我将在底部链接。
这有助于我们将所有 embed.Files 集成 到库的其他部分,而
无需这些部分明确知道 embed.Files 是什么。
他们只采用通用文件系统。
文档中有详细信息,我们将 在 Reddit 上进行问答,因为 Reddit 比 GitHub 具有更好的
线程支持,而且 上次似乎运行得很好。
非常感谢您的观看。
原文链接: https://dashen.tech/2023/12/10/go-embed-draft-design-来自Russ-Cox视频/
版权声明: 转载请注明出处.