Go: 嵌入、cmd/go:添加对嵌入文件的支持

创建于 2020-09-02  ·  114评论  ·  资料来源: golang/go

7 月, @bradfitz和我发布了嵌入文件的设计草案。 该文档链接到视频、原型代码和 Reddit 讨论。

对该设计的反馈非常积极。

我建议采用 Go 1.16 的嵌入式文件草案设计,并在讨论中建议增加一项,以简化直接访问单个嵌入式文件中字节的情况。

只要文件导入"embed" (如果需要, import _ "embed" ),就允许使用//go:embed命名单个文件(不允许使用 glob 模式或目录匹配)来初始化一个普通的string[]byte变量:

//go:embed gopher.png
var gopherPNG []byte

导入需要将文件标记为包含//go:embed行并需要处理。 Goimports(和 gopls 等)可以学习这个规则,并根据需要自动在任何带有//go:embed文件中添加导入。

嵌入式文件设计依赖于文件系统接口设计草案,我也在#41190 中建议采用。

这个问题是_only_关于采用嵌入式文件设计,假设也采用了文件系统接口设计。 如果这个提议在文件系统接口设计之前被接受,我们就干脆等待文件系统接口设计之后再开始登陆。

Proposal Proposal-Accepted

最有用的评论

共识没有改变,所以接受。

所有114条评论

在不导入embed情况下使用//go:embed指令会出错吗?

@jimmyfrasche是,第五到最后一个在列表子弹https://go.googlesource.com/proposal/+/master/design/draft-embed.md#go_embed -directives。

@rsc也许我在草稿中遗漏了它,但我看不到嵌入您在评论中提到的单个文件的能力。
另外,您是否也可以将单个文件作为常量字符串嵌入?
感谢这个伟大的提议。

@pierrec它不在文档草案中(“一个补充”是上面评论中的文本)。 Const 字符串最终会在决定程序是否进行类型检查方面发挥作用,这意味着所有类型检查器都需要了解 //go:embed'ed consts。 相比之下,如果我们坚持使用 vars,类型检查器就没有那么明智了,可以单独留下。 似乎我们应该坚持使用 vars。

你想要一个 const 而不是 var 有什么特别的原因吗? 就效率而言,使用它们应该大致相同。 (无论如何,对 const 字符串的引用最终会编译为对隐藏变量的引用。)

感谢您的解释。 目前我倾向于将静态资产嵌入为常量字符串,这就是我问的原因。 我对 vars 也很好!

有趣,所以我可以做这样的事情:

//go:embed version.txt
var Version string

甚至可能有//go:generate注释来生成 version.txt。 这将减少 makefiles/ldflags 的大量用例。

如果找不到文件是错误吗? 如果是这样,从技术上讲,错误发生在哪里? 链接时间?

我们能否确保 go:embed 在 go:generate 之后运行,以便我们可以轻松地生成版本等?

我们能否确保 go:embed 在 go:generate 之后运行,以便我们可以轻松地生成版本等?

根据我的理解, go:generate将与go generate发生,而go:embed将与go build

@carlmjohnson是的,在 foo 不存在的情况下说//go:embed foo总是错误的。
编译包含该行的源文件时会发生错误。
(如果您在编译该源文件后删除 foo,您仍然无法进入链接步骤 - go 命令会注意到包需要重建,因为 foo 已被删除。)

我认为如果不说 ETag,这个提议是不完整的。
https://old.reddit.com/r/golang/comments/hv96ny/qa_goembed_draft_design/fzi0pok/

@tv42 ,是的,我们会让 ETag 工作。 我不确定那是什么形状,但我们会的。
(也在 https://github.com/golang/go/issues/35950#issuecomment-685845173 上确认。)

两件三个事情我已经从工作注意到mjibson/esc

  • 由于go:embed不需要生成用于嵌入为只读文件系统的 go 文件,因此它将消除更改go:generate ed 文件上的时间戳的痛苦,这些文件无视git porcelain对 CI 的测试-非常好
  • 我在提案中没有找到但需要的一件事是能够在开发周期中实时重新加载嵌入的文件。 使用mjibson/esc我目前可以这样做,指示它使用本地文件系统(尽管它不会获取新文件)并使用构建标记更改行为。 我想知道提案中可以包含哪些内容?
  • 更新我记得的另一件事是esc需要能够透明地剥离(部分)基本路径,以便例如将资产文件夹导出为 Web 根。

事后思考:我想第二点可以与io/fs提案一起解决,我将使用嵌入式文件系统或实时文件系统进行包含? 将路径剥离实现为io/fs中间件?

@andig在通过 HTTP 提供文件系统时,您已经可以去除前缀。 我同意实时重新加载可以由包装io/fs第三方库完成。

还有一件事:如果我理解正确, embed 会将文件本地视为包并禁止.. 。 我目前的设计有/assets/server/ ,后者包含服务器的代码,今天托管生成的文件。 有了这个提议,嵌入将需要移动到根文件夹,因为资产将无法从服务器访问。 这强加了与普通导入不同的可访问性约束。 我想知道出于安全原因这是否是必要的,或者是否应该通常允许模块本地嵌入。

还有一件事:如果我理解正确, embed 会将文件本地视为包并禁止.. 。 我目前的设计有/assets/server/ ,后者包含服务器的代码,今天托管生成的文件。 有了这个提议,嵌入将需要移动到根文件夹,因为资产将无法从服务器访问。 这强加了与普通导入不同的可访问性约束。 我想知道出于安全原因这是否是必要的,或者是否应该通常允许模块本地嵌入。

您可以在您的资产目录中创建一个 emed.go 文件,并将资产作为自己的包提供给您的程序的其余部分。

另一个明确的目标是避免语言变化。 对我们来说,嵌入静态资产似乎是一个工具问题,而不是语言问题。

同意。 在我看来,在语言中添加语法糖以支持这种工具更改是一种语言更改。 我相信这对其他人来说是显而易见的,但这实际上是注释即代码。

我强烈认为魔法/糖会损害语言的简单性和可读性; 很容易错过嵌入文件的神奇注释。 虽然对此的回应很容易是“好的,然后不要使用它”,但这种变化意味着审阅者仍然必须对使用此功能的其他人保持警惕,并且必须记住围绕变量声明的评论可能会破坏构建或失败编译时。

我相信这会增加混乱,降低语言可用性,并会导致不透明的大型二进制文件没有明显的好处(关于最后一个问题,这甚至会导致由于纯文件更改而重新构建二进制文件的反模式)。 如果go mod允许--withNonGoCodeAssets ,我相信这将解决大多数不想编写更复杂构建管道的开发人员的需求(我假设最终用户分发是一个较小的子集)用户的问题)。

@tristanfisher ,我理解你关于语言与工具变化的观点。 肯定是在这条线附近。 我认为它更像是工具更改的原因是语言规范不受影响——程序是否有效不会改变,类型检查过程不会改变。 所有更改都是注释后该变量的初始值。 这种方式有点像链接器的 -X 标志,它可以设置字符串类型的顶级 var 的初始值。 我们可以不同意; 我只是想明确我的定义并解释我所做的区别。

至于膨胀,我想我们必须看看,但我不认为程序会变得比现在大得多。 人们_已经_运行工具将任意文件转换为 Go 代码,将它们签入他们的存储库,并让编译器构建它们。 该设计从这个过程中消除了一些开销,但没有启用任何新的东西。 也许现在人们会滥用它,因为它更容易做到,但总的来说,我不认为这会成为什么大问题。 (并且如果某些依赖项嵌入了如此大的内容以致使您的二进制文件膨胀,您始终可以选择不使用该依赖项。)

至于由于普通文件更改而导致的重建,唯一可以触发重建的文件是您自己的顶级模块中的文件,因为依赖项是不可变的。 如果您发现重建比您希望的更频繁,唯一的解释是 (1) 您正在嵌入文件和 (2) 您正在修改这些文件。 您将完全控制对任何一个原因做些什么。 (如果依赖项选择使用的内容以某种方式迫使您进行额外的重建或其他费用,那将完全是另一回事。但这里的情况并非如此。)

@rsc我同意我们可以不同意,我感谢您的回复。 我的感觉是,如果它默认包含在标准工具中,并且注释会导致变量的隐式初始化,那么它就是语言更改。 在那场辩论之外,我想我的恶心感觉是围绕更多指令作为需要由(人类)代码阅读器记住的“神奇”评论。 这可能会导致通过在构建时处理的块注释添加新功能的荒谬结论。

也就是说,如果这被添加到生态系统中,我会很感激需要导入embed - 这比在审计代码时“嘿,抬起头来”什么都不做要好。 我认为允许非 .go 的go mod将解决大多数用例(我想大多数人会为网络服务器使用 glob 文件)并且也将完全存在于工具中。

我认为您关于链接器的观点很好。 这也有助于解释我对此的感受:如果最终用户(例如不是简单地导入包的人)做出决定,那么就不会对出现的大量非代码感到惊讶。 我的担忧源于审查/配对他人的工作和“技术领先”的责任,这就是为什么我觉得有必要做出回应。

我认为“我们将不得不看到”总结得很好(我对膨胀/滥用更加愤世嫉俗)。

今晚我将通读设计草案,到目前为止,从 TinyGo 的角度来看,它看起来不错。

我只想澄清一件事:

另一方面,像TinyGoU-root这样的项目目标系统的 RAM 多于磁盘或闪存。 对于这些项目,在运行时压缩资产和使用增量解压可以显着节省成本。

我不知道 U-root,但对于 TinyGo 而言,主要目标是通常具有比 RAM 多得多的闪存(通常是 8 或 16 倍)的微控制器。 快速浏览一下设计草案似乎表明这个想法是将文件保存在只读存储器中,这对这些目标很有效:嵌入的文件可以直接从闪存中读取。 TinyGo 目标很可能不希望在运行时解压缩文件。

这依赖的 io/fs 提案似乎因 Readdir/FileInfo 问题而被阻止,正在讨论中 #41188 和之前的 #40352。

我在https://github.com/golang/go/issues/41188#issuecomment -686283661 中起草了一个 API 来替换它们

@andig

我在提案中没有找到但需要的一件事是能够在开发周期中实时重新加载嵌入的文件。

embed.Files 实现了 fs.FS,因此您需要做的就是使用 dev vs !dev 构建标记在 embed.Files 和真正的 FS 之间切换变量。

我提交了#41265。 它为 io/fs 提供了一个新的 ReadDir() API。

我和@tristanfisher有类似的担忧。 Go 长期以来一直使用魔术注释作为编译器指令(从一开始?),但它们是针对极端情况的,它们很少出现在代码中。 鉴于在 Go 二进制文件中嵌入静态内容的流行, //go:embed可能更常见。 也许是时候为编译器指令考虑不同的语法了?

只是提醒一下,更改 Go 语法的成本非常高。 例如,几乎每个 Go 工具都需要更新和/或修复以支持新语法。

我不认为它们是神奇的评论。 以//go:开头的行是指令,可以在规范中这样定义。 //go:embed@embed[[embed]]或任何其他数量的语法变体之间没有很多语义差异,除了//go:前缀已经被视为非-Go 工具的代码。 (例如,我的编辑器以不同的方式突出显示这些行)

@mvdan如果此提议发生,则 Go 语法已更改。 它只是在不破坏现有工具的情况下进行了更改。 也许这看起来很迂腐。

@iand我对编译器指令的特定语法并不挑剔。 我只是认为它需要在某个时候正式化并指定规则。

我认为这个提议是个好主意。 它解决了一个常见的问题。 我担心的是,采用它的成本应该更明确一些。

@jonbodner我分享你对魔术评论的担忧。 但在某种程度上,规则是由 #37974 指定的。

@networkimprov ,这不是 io/fs 提案。 请停止在这里评论 ReadDir。

@jonbodner

我对编译器指令的特定语法并不挑剔。 我只是认为它需要在某个时候正式化并指定规则。

我只想指出,当我们决定使用//go:来标记 Go 工具链指令时
我们在 2012 年添加了(有限使用) //go:nointerface注释。
我们在 2013 年为程序集作者添加了//go:noescape
我们在 2014 年增加了//go:generate
我们也可能在 2020-2021 年
还有其他的; 这只是亮点。
如果有帮助,您可以将//go:视为 C 中的#pragma

在这一点上,公约已经非常成熟。
我们在 2012 年选择了这种语法,因为
(1) 显然不是对一个人的评论;
(2) 不知道注释的工具会忽略它们,因为它们是注释; 和
(3) 它推广到其他工具 (s/go/yourtool/)。

正如 Ian 所说,#37974 正式化了确切的通用注释语法,这是值得的。

根据上面的讨论,这似乎是一个可能的 accept
(同样,假设但与 FS 提案分开。)

共识没有改变,所以接受。

我迫不及待地想尝试嵌入 - 这是否已经在 master 上进行了测试,或者是否有任何计划在 1.15 周期内将其作为实验发布?

@andig ,Go 1.15 已经出来了。 我仍然希望这将在 Go 1.16 中并在本月登陆开发分支。

@rsc 1.16 可用吗?

@septs ,不,我们仍在开发 Go 1.16。 代码冻结时间为 10 月 31 日,目标发布日期为 2 月 1 日。

最快的 2021Q1 或 2021Q2 发布?

@septs请停止在此线程中询问有关 Go 版本的问题。 超过 20 人关注它并得到通知。 请参阅https://golang.org/wiki/Questionshttps://github.com/golang/go/wiki/Go-Release-Cycle。

更改https://golang.org/cl/243941提到这个问题: go/build: recognize and report //go:embed lines

更改https://golang.org/cl/243940提到这个问题: go/build: refactor per-file info & reader

更改https://golang.org/cl/243942提到这个问题: embed: implement Files

更改https://golang.org/cl/243944提到这个问题: cmd/compile: add //go:embed support

更改https://golang.org/cl/243945提到了这个问题: cmd/go: add //go:embed support

实施审查中出现的一个细节是,“文件”作为单数名词非常尴尬(“文件持有......”)。

选择 embed.Files 作为名称早于 io/fs 提案以及对字符串和 []byte 的支持。
鉴于这两种发展,解决“A 文件保留”问题的一种看似合理的方法是将其称为 FS 而不是 Files。

那么嵌入和打印数据的三种方式是:

import "embed"

//go:embed hello.txt
var s string
print(s)

//go:embed hello.txt
var b []byte
print(string(b))

//go:embed hello.txt
var f embed.FS
data, _ := f.ReadFile("hello.txt")
print(string(data))

你得到的东西似乎更清楚:一个字符串、一个 [] 字节或一个 FS。
也就是说,embed.F* 的大部分功能都来自于它是一个 fs.FS,并且将其称为 FS 比将其称为 Files 更清楚。

我已经在我最新的 CL 实现包嵌入草案中进行了更改,但我想回到这里,看看是否有任何对名称更改的反对意见。

(一个更彻底的改变是用var f fs.FS而不是var f embed.FS ,但这将排除在f除 Open 之外的任何方法。例如,上面有ReadFile很方便,而且是不可能的。总的来说,我们已经了解到,与直接使用接口类型相比,将具体类型用于稍后可能想要添加方法的东西是很好的面向未来的证明。)

我认为重命名是一个很好的改变。

关于更激进的变化:

  • 如果我们使用fs.FS ,我们还需要embed包吗? 我猜动态值仍然必须有某种类型,它存在于某个包中? 我发现不必添加包的想法是加分项。
  • 我不认为“我们不能添加方法”超级说服力,因为国际海事组织f.ReadFile(…)不显著低于更方便fs.ReadFile(f, …)
  • 我同意具体类型一般来说更好,所以保留它是一个加分embed.FS
  • 另一个问题: embed.FS使用指针接收器还是值接收器? IMO 必须传递&f很尴尬,使用值接收器有点出乎意料。 不过,我们也可能允许var f *embed.FS 。 如果变量有一个接口类型,这个问题就不存在了。

总的来说,我仍然同意使用具体的embed.FS更好 - 如果没有别的,那么用于文档目的。

既然您已经提到了,我想我还没有说清楚:我们可以嵌入目录,对吗?

是的,作为实现 fs.FS 的 embed.FS。

@Merovius , embed.FS 使用值接收器。 embed.FS 是一个包含单个指针的单字结构,因此这样做没有实际开销,但这意味着您可以分配它们并使用它们,而不必担心 *s 和 \&s 无处不在。

@chabad360 ,是的,您可以嵌入目录。

符号链接呢?

@burik666 ,有关详细信息,请参阅https://golang.org/s/draft-embed-design ,但不可以,您不能嵌入符号链接。

是否可以嵌入和使用动态 C 库? 如果是这样,我们将如何在#cgo标头中使用嵌入路径,例如: #cgo LDFLAGS: -L./lib -lmylib -Wl,-rpath=./lib

@benitogf我认为唯一真正的方法是将它们写入磁盘并使用dlopen 。 我无法想象你怎么能告诉动态加载器如何找到嵌入的文件。 此外,如果您想捆绑 C 代码,静态链接无论如何似乎更合适,不是吗?

@benitogf Embedding 可让您方便地将文件从磁盘放入程序中的 [] 字节,仅此而已。
如果您有办法以 [] 字节的形式使用程序中已经存在的动态 C 库,那么嵌入将帮助您在那里获得磁盘文件。 否则,没有。

无论如何,静态链接似乎更合适,不是吗?

@Merovius同意,但我有几个用例与只提供动态库的供应商合作

如果您有办法以 []byte 的形式使用程序中已经存在的动态 C 库
唯一真正的方法是将它们写入磁盘并使用 dlopen

将嵌入式库从 []byte 写入文件系统并使用 dlopen 似乎没问题,尽管在构建/运行时可选择将嵌入式文件“转储”到文件系统,以便#cgo标头可以访问它们会很有用,不仅对于 cgo 恕我直言

现在试试这个; go:embed指令的一个缺点是,如果我嵌入build/* ,文件名仍然具有前缀build/ 。 如果我想然后通过http.FS为该目录提供服务,则没有简单的方法可以_add_ 如果需要访问它们所需的前缀(无需编写包装器,然后遇到需要列出每个潜在方法的问题) FS 可能有...)。

例如:

//go:embed build/*
var buildDir embed.FS

// Serve some SPA build dir as the app; oops, needs to be build/index.html
http.Handle("/", http.FileServer(http.FS(buildDir)))

// or

//go:embed static/*
var staticDir embed.FS

// Oops; needs to have a static prefix.
http.Handle("/static/*, http.StripPrefix("/static", http.FileServer(http.FS(staticDir))))

// Could be this, but only because the prefix happens to match:
http.Handle("/static/*, http.FileServer(http.FS(staticDir)))

我知道这样做的目的是可以编写go:embed foo/* bar/* baz.ext并获取所有这些文件,但我认为简单地嵌入一个目录并通过 http 包将其作为静态资产提供服务是很常见的。 我希望这是一个问题,因为人们从http.Dir("static")pkger.Dir("/internal/web/static")之类的前缀已经处理过的东西切换到新的embed.FS

我不太确定如何提交这个文件,因为它有点与embedio/fsnet/http的相互作用。

@zikaeroh编写http.Handler包装器也可以在那里工作,对吗? 这只是一种方法,甚至还有http.HandlerFunc 。 也许标准库甚至可以提供一个镜像http.StripPrefix (类似于http.AddPrefixhttp.ReplacePrefix )。

潜在地,尽管修改 HTTP 请求以绕过 FS 实现感觉有点奇怪(而不是笼统的“给我一个作为另一个 FS 的子目录的 FS”,这对于可选方法来说并不简单)。 这不是最有效的方法,剥离然后再次添加另一个前缀(给定http.Request副本),但我稍后会尝试。 我想,它至少与您必须处理请求的当前方案没有_不同_。

我还有一些其他地方使用静态数据而不是通过 http 包,我必须为这些地方提供类似的修复程序。

如果我必须看看它是如何实施的,我可以在哪里查看。 正在实施的分支?

之前建议就地嵌入文件,即在构建目录中执行此操作,然后将其导入。 这将去除构建前缀。 然后使用处理程序添加所需的前缀。 我不确定如何从嵌入本身中排除进行嵌入的 go 文件。 见https://github.com/golang/go/issues/41191#issuecomment -686621090

之前建议就地嵌入文件,即在构建目录中执行此操作,然后将其导入。 这将去除构建前缀。 然后使用处理程序添加所需的前缀。 我不确定如何从嵌入本身中排除进行嵌入的 go 文件。 见#41191(评论)

不幸的是,这对其他工具生成的目录没有好处,例如 webpack 构建或 CRA 的输出(它们通常事先被清理,而不是签入)。 我宁愿破解文件名。

之前建议就地嵌入文件,即在构建目录中执行此操作,然后将其导入。 这将去除构建前缀。 然后使用处理程序添加所需的前缀。 我不确定如何从嵌入本身中排除进行嵌入的 go 文件。 见#41191(评论)

不幸的是,这对其他工具生成的目录没有好处,例如 webpack 构建或 CRA 的输出(它们通常事先被清理,而不是签入)。 我宁愿破解文件名。

如果您使用像 webpack 这样庞大的插件系统,则只需安装另一个 webpack 插件即可轻松生成沿资产本身的 embed.go。 如果您使用更简单的 makefile 或 shell 脚本,从那里生成 .go 文件也很容易。

@zikaeroh

与笼统的“给我一个是另一个 FS 的子目录的 FS”相反,这对于可选方法来说并不简单

应该很简单吧处理包装问题是设计过程的一部分。 特别是,包装器应该通过调用fs适当的辅助函数来实现所有可选方法。 如果这不起作用,那就令人担忧了,如果能获得一些详细信息,那就太好了。

此外,IMO,这种包装器(去除前缀)的实现最终应由io/fs (类似于io.LimitWriter等)提供。 我认为它还没有发生的唯一原因是时间。

@andig这样做的问题是包含 embed-directive 和变量的 Go 文件然后也可以从 FS 看到(并且将由 HTTP 提供服务或可能以其他方式公开)。

这样做的问题是包含 embed-directive 和变量的 Go 文件然后也可以从 FS 看到(并且将由 HTTP 提供服务或可能以其他方式公开)。

一种补救方法可能是添加从嵌入中排除特定文件/文件夹的功能( @rsc ?)

一种补救方法可能是添加从嵌入中排除特定文件/文件夹的功能( @rsc ?)

该提案在一个多月前被接受并已经实施; 我不认为像能够排除路径这样的大型设计更改在这一点上是合理的。 如果您对已实现的设计有无法解决的问题,我建议您提交一份包含详细信息的单独错误报告,然后可以在最终 1.16 版本发布之前对其进行跟踪。

@Merovius

应该很简单吧处理包装问题是设计过程的一部分。 特别是,包装器应该通过调用 fs 中适当的辅助函数来实现所有可选方法。 如果这不起作用,那就令人担忧了,如果能获得一些详细信息,那就太好了。

Glob的前缀剥离如何工作?

@icholy我假设类似

func (f *stripFS) Glob(pattern string) (matches []string, err error) {
    matches, err = fs.Glob(f.wrapped, path.Join(f.prefix, pattern))
    for i, m := range matches {
        matches[i] = strings.TrimPrefix(m, f.prefix+"/")
    }
    return matches, err
}

也许需要一些额外的照顾。

在 gotip 上玩这个,我注意到它将包含 .DS_Store 文件。 我想这几乎是不可避免的,但我确实担心包含点文件会导致意外包含文件。 也许文档应该对此发出强烈警告?

我的 shell 没有在*包含点文件,所以如果我想包含它们,我必须使用* .* 。 这可能是一种(也许同样令人惊讶)提供一定程度控制的方式。

我不知道该怎么想 - IMO,点文件不应该被模式真正区别对待,但是 OTOH .DS_Store示例似乎真的应该解决。

为什么不直接做git clean -dffx && go build ? 如果 DS_Store 文件在 git 中,那么它们将包含在构建中。 如果他们不是,他们就不会。 这也适用于 gitignore。

无论如何,您应该使用干净的 VCS 结帐进行构建。 如果您添加随机临时文件,它们可能会进入最终版本,并且您不可能知道人们最终会得到哪些文件。 我们可能想要记录下来。

@mvdan我原则上同意,但在实践中,我担心如果没有警告很多人会被脏构建烧毁。 我不想看到有人没有意识到他们的 .env 文件被错误嵌入的秘密泄漏。 我已经看到了大量与 PHP 托管相同错误的示例,尽管通过告诉 Apache 排除点文件可以轻松防止它。


回复:嵌入 http.FileServers

如果您使用像 webpack 这样庞大的插件系统,则只需安装另一个 webpack 插件即可轻松生成沿资产本身的 embed.go。

这是真的,但它非常繁琐。 embed.FS 的目的是减少对疯狂 Makefile 解决方法的需求。 我认为简单的解决方案是让fs.WithPrefix(string) fs.FS将 FS 锁定到子目录中。 我以为提案中对此进行了一些讨论,但现在找不到了。 也许它只是在 Reddit 上提出来的。

一种补救方法可能是添加从嵌入中排除特定文件/文件夹的功能( @rsc ?)

它可能是这样的

//go:embed static
//go:embed-exclude .*
var staticFiles embed.FS

embed-exclude 指令可以对接受的文件进行全局过滤并删除任何匹配项......

如果我们想避免在此提案中添加更多内容,它也可能是一个 lint 规则,用于检查嵌入式文件系统是否存在潜在的意外点文件并警告您,以便您可以修复您的构建以删除它们。

或者默认情况下排除点文件,除非通过添加 .* 或类似内容特别提及它们。

这仍然不会处理暴露 assets.go 文件。 至于已经实施的提案,请注意在讨论阶段提出了关于资产生成的问题。 暴露(否则为空,除了 embed 指令)assets.go 可能并不危险,但没有它会更干净。 像往常一样,可以应用各种解决方法。

暴露(否则为空,除了 embed 指令)assets.go 可能并不危险,但没有它会更干净。

我同意这不太可能是一个问题,如果我们可以轻松地正确配置,我不希望看到任何封闭的源代码由于配置错误而意外泄露。

我不想看到有人没有意识到他们的 .env 文件被错误嵌入的秘密泄漏。

如果有人使用//go:embed static/*并且有static/.envstatic/.super-secret ,您不是说用户真的打算包含这些文件吗? 否则,为什么它们会在静态目录中?

我知道这取决于用户的期望以及*在大多数情况下的含义,但我个人认为https://golang.org/pkg/path/filepath/#Glob语义是我们唯一的好处选项。 这是最简单的,也是大多数 Go 用户在开发 Go 的背景下会习惯的。

不过,我认为警告嵌入*的危险在任何情况下都是一个好主意,因为在大量情况下,可以通过使用更具体的全局变量(如*.png来减少出错的机会.

此外,我仍然鼓励您提交一个单独的问题,从错误报告的角度编写,可以针对 1.16 版本进行跟踪。 这个提议被接受并实施了,所以我想它很快就会被关闭。 例如,您可以将错误报告表述为:嵌入式文件支持很容易导致包含非预期文件(并给出一些示例)。

如果有人使用 //go:embed static/* 并且有一个 static/.env 或 static/.super-secret,你不是说用户真的想包含这些文件吗? 否则,为什么它们会在静态目录中?

有大量的极端情况,例如您使用 vim 打开了一个编辑会话,但没有关闭它,并且它创建了.*.swp文件,其中包含一些您不希望任何人看到的内容。

将讨论移至#42321。

Prisma 团队会为我们的Database Go 客户端高度赞赏这一点,因为我们在运行时使用了一个用 rust 编写的查询引擎,并且需要以某种方式在构建的 go 二进制文件中。

我们目前的做法是在 go:generate 时将二进制文件打包成 .go 文件,但文件大小比二进制 .gz 文件大得多。

Native embeds 会让这个更好,所以我们可以直接将 .gz 文件嵌入到最终的 go 二进制文件中。

如果有人使用//go:embed static/*并且有static/.envstatic/.super-secret ,您不是说用户真的打算包含这些文件吗?

我不会。

$ mkdir z
$ touch z/.secret z/intended
$ ls z/*
z/intended
$ ls z
intended

请参阅我后来在https://github.com/golang/go/issues/42328#issuecomment -720169922 中的评论。

我喜欢使静态文件/模板可嵌入的想法,这绝对可以大大提高 go 开发人员的生产力。

但是除了重用这个应该是注释的// ,我们是否应该创新另一个标签(例如@或其他什么)?

到目前为止,我猜//已经被过度使用了,想想这些:

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:webhook:verbs=create;update,path=/validate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=false,failurePolicy=fail,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,versions=v1,name=vcronjob.kb.io
// +optional
...

但是除了重用这个应该是注释的// ,我们是否应该创新另一个标签(例如@或其他什么)?

这是一个单独的讨论,虽然指令不是注释会很好,但很多 go1 兼容代码已经依赖于它,所以它很可能不会改变。

但是除了重用这个应该是注释的// ,我们是否应该创新另一个标签(例如@或其他什么)?

我试图找到它但失败了,但我记得有一个决定对这些类型的指令标准化//go:<word>
考虑到明确地收敛于它们的决定,改变语法似乎不是一个好主意。
(此外,当然,它们是专门的注释,以便编译器忽略它们 - 这些指令特定于 go 工具,因此它们不应该泄漏到正确的语言中)

我看到一个关于embed.FS支持 ETag 的io/fs问题的提及: https :

我尝试为它运行一个测试,但我没有看到ETag响应标头集。 也许我误解了用法。 我应该期待在这里看到一个吗? https://play.golang.org/p/Wq5xU5blLUe

我不认为它。 http.ServeContent (由http.FileServer )检查 ETag 标头,但不会设置它,AIUI。

在上面的评论中, Russ 说 ETag 将起作用。 难点在于如何让embed.FShttp.FileServer embed.FS通信设置ETag或其他缓存标头的必要信息。 可能应该有一个单独的跟踪问题。

就个人而言,我认为embed.FS应该使用相关模块最后一次提交的时间作为ModTime 。 这将大致对应于debug.BuildInfo ,因此它不会影响重现性。 虽然我不确定如何为与标记版本不对应的提交设置它,但它仍然会留下一个问题,即为来自脏工作树的构建设置什么。

但是,我相信@rsc有一个很好的解决方案:)

我不确定我是否理解关于“提交时间”的评论; 如果模块源是一个 zip 文件,则没有“提交”。 我没有看到debug.BuildInfodebug.Module 中提到的任何时间。

更重要的是,我认为任何基于时间戳的机制都比适当的(基于内容的散列)etag 更差。

@tv42每个模块版本要么是 a)从标签(指向提交)派生的语义版本,要么 b)包含提交哈希的伪版本。 我认为? 至少在 git 中。 我可能误解了一些东西。

更重要的是,我认为任何基于时间戳的机制都比适当的(基于内容的散列)etag 更差。

我不确定。 它要么需要一些侧信道来传达散列,要么服务器需要根据请求计算文件的散列(这似乎非常昂贵)。 毕竟, net/http先验不知道fs.FS内容是否会改变。 基于散列的的ETag的最终结果可能证明添加这样的侧信道(如可选的接口)的成本,但它似乎并不明显严格把好。

另外,我认为,至少也是一个基于时间的方法支持将意味着你可以与更多的客户合作。 不过,我没有任何数据支持这一点(即我不知道是否以及有多少客户端可能支持If-Modified-Since但不支持ETag ,反之亦然)。

但实际上,我不太关心选择哪种方法。 我只想提及使用标记模块版本的时间的选项。

@Merovius Go 模块是根据 zip 文件指定的。 Git 是用于构建的通用源这一事实不会改变规范。 zip 的时间戳为零:

$ unzip -v ~/go/pkg/mod/cache/download/golang.org/x/crypto/@v/v0.0.0-20200510223506-06a226fb4e37.zip|head
Archive:  /home/tv/go/pkg/mod/cache/download/golang.org/x/crypto/@v/v0.0.0-20200510223506-06a226fb4e37.zip
 Length   Method    Size  Cmpr    Date    Time   CRC-32   Name
--------  ------  ------- ---- ---------- ----- --------  ----
     345  Defl:N      233  33% 1980-00-00 00:00 237856c8  golang.org/x/[email protected]/.gitattributes

随附的*.info文件中似乎确实有一个时间戳,但我不知道它是否可靠或可用于工具。

$ cat ~/go/pkg/mod/cache/download/golang.org/x/crypto/@v/v0.0.0-20200510223506-06a226fb4e37.info; echo
{"Version":"v0.0.0-20200510223506-06a226fb4e37","Time":"2020-05-10T22:35:06Z"}

即便如此,在主模块(您在其中运行go build那个)中应该使用什么时间戳:embed 使用?

就个人而言,在永久嵌入二进制文件的静态文件的上下文中,散列似乎在各个方面都优于时间戳(除了 ETag 存在之前、HTTP/1.1 之前、90 年代的遗留支持),并且没有可能的来源分布式系统和并发构建中的混淆。 我严重怀疑不再存在不理解 ETag 的客户,他们肯定不会进行 HTTP/2 转换。

对我来说,传达哈希值的侧信道听起来是正确的; 一个可选的 FS 接口,用于返回文件的哈希值。 (也许另一个请求特定的哈希算法,如果他们碰巧拥有它?。一些服务用例确实特别需要 sha256/sha1/md5,而不仅仅是“一些哈希”)。 没有足够便宜的答案的 FS 可以选择不实施它。

(尽管现代哈希即使在加密安全的情况下也是千兆字节/秒/核心,但对于不太安全的哈希则是几十千兆字节/秒,并且基于 stat 调用易于缓存。只要支持 ETag 无处不在,伙计们。)

我今天一直在考虑这个提议。 我很高兴看到 Go 工具链中包含此功能,我感谢迄今为止提案中的所有想法和努力。 感谢您提出这一建议并推动这一进程。

我有两个问题,然后是一个建议(感谢这已经过了提案讨论期,并且已经进入发布冻结状态):

允许的类型

我注意到当前代码期望变量被精确地声明为 ~ embed.FS ~、 string[]byte 。 特别是,这些是不允许的: []uint8 、 ~ FS点导入后的“嵌入”~,或任何其他使用类型别名构造的相同类型。 (编辑:我忘记了点导入在 gc 中是如何工作的,并且误读了检测embed.FS的代码。)

这是故意的还是错误?

[]byte类型变量的语义。

我没有看到任何提及[]byte类型变量的预期身份语义是什么。 特别是对于函数范围的变量。 这对于string - 和embed.FS类型变量并不重要,因为它们引用了可以安全地进行重复数据删除的不可变数据。 但是了解[]byte类型变量的预期语义很重要。

使用当前的实现,下面的测试程序打印false true (当foo.txt为非空时)。 这是有意的/有保证的吗?

package main

//go:embed foo.txt
var a []byte

//go:embed foo.txt
var b []byte

func f() *byte {
    //go:embed foo.txt
    var x []byte
    return &x[0]
}

func main() {
    println(&a[0] == &b[0], f() == f())
}

我认为//go:embed变量更像 Go 的语义是复合文字的语义:每次执行都会产生一个新副本。

如果对此的正确语义没有达成共识,我们总是可以将其放在函数范围内, []byte -typed 嵌入 Go 1.16 的错误:用户仍然可以声明包级变量,如果他们想要当前的语义(每个源声明一个字节片),或者使用string -typed 变量并转换为[]byte如果他们想要复合文字语义。 然后我们可以稍后重新审视用户会从哪些行为中受益。

避免更多的//go:指令

我建议不要为会影响程序语义的最终用户添加//go:指令,并且我不认为支持//go:embed不是普通 Go 语法的论据令人信服。 我恭敬地鼓励在 Go 1.16 发布之前重新考虑这个决定。 (再次,我很欣赏这个请求有多晚。)

我将首先指出我在CL 276835处有一个有效的概念验证 CL,它将嵌入 API 更改为:

//go:embed foo.txt bar.txt
var x embed.FS

var x = embed.Files("foo.txt", "bar.txt")

类似地,函数embed.Bytesembed.String可用于嵌入单个文件并将其作为[]bytestring值获取。

回应

同样, embed.Files 变量可以是全局变量或局部变量,具体取决于上下文中更方便的内容。

拥有embed.Files等也允许在表达式上下文中使用它们,这可能更方便。

在未导入“embed”的源文件中使用 //go:embed 是错误的(违反此规则的唯一方法涉及类型别名欺骗)。

对于embed.Files等,由于包嵌入导出的函数,这是一个错误。

Goimports(和 gopls 等)可以学习这个规则,并根据需要自动将导入添加到任何带有 //go:embed 的文件中。

不需要特殊的 goimports 或 gopls 逻辑来意识到导入“嵌入”是修复embed.Files等使用的正确方法。

这种方法解决了类型检查问题——它不是一个完整的语言变化——但它仍然具有显着的实现复杂性。

值得注意的是, CL 276835是代码的净删除。 特别是,编译器代码(必须在 gccgo 和其他编译器中重新实现)要简单得多。

我也希望教 go/types 了解embed.Files等的细微语义(即,它们只接受字符串文字参数),而不是教它//go:embed

go 命令需要解析整个 Go 源文件以了解哪些文件需要可用于嵌入。 今天它只解析到导入块,从来没有完整的 Go 表达式

在 CL 276835 中,行为与 tip 相同:go 命令解析整个 Go 源文件以查找导入包嵌入的文件,仅解析未导入包的文件的导入。

诚然,对于导入嵌入的文件,CL 276835 会执行完整的解析和遍历,而tip 会针对//go:embed注释执行更有效的字符串扫描。 我认为,如果需要,可以使用更优化的一次性算法来查找embed.Files调用。

用户也不清楚这些特殊调用的参数有什么限制:它们看起来像普通的 Go 调用,但它们只能接受字符串文字,而不是由 Go 代码计算的字符串,甚至可能不能接受命名常量(或者其他go 命令需要一个完整的 Go 表达式评估器)。

对我来说,这与//go:embed指令似乎没有本质区别:用户在第一次使用它之前并不期望他们被允许在那里使用什么参数。 此外,无论哪种方式,如果用户使用不当,都会收到编译器错误消息,但 IDE 和其他工具会自动为embed.Files等提供更好的 godoc 和交叉引用。

@mdempsky FSembed点导入之后是相同的类型。 所以,这种特殊情况似乎是一个直接的“是的,很好”(尝试过它已经有效)。 同样, byte是一个别名uint8 ,所以[]uint8也是同一类型[]byte ,虽然有点出人意料的是,这,现在工作. 但是,我认为实现的语义现在很好 - 如果需要,我们总是可以在以后允许更多类型和/或别名和/或什至“相同的基础类型”。

我认为 //go:embed 变量的更像 Go 的语义是复合文字的语义:每次执行都会产生一个新副本。

我倾向于同意,这也是我的期望。 起初我认为这可能是逃逸分析的产物,编译器意识到你没有改变数据,但没有:

func f() {
    //go:embed foo.txt
    var x []byte
    fmt.Printf("%q\n", x)
    x[0] = 'x'
}

func main() {
    f()
    f()
}

但是,“每个 var 声明都会创建一个新变量”也有它的问题,因为它意味着这样的代码

func ServeIndex(w http.ResponseWriter, r *http.Request) {
    //go:embed "index.html"
    var x []byte
    http.ServeContent(w, r, "index.html", time.Now(), bytes.NewReader(x))
}

将不必要地分配和复制。 也许这很好,并且会被更清晰语义的好处所抵消。 但也许这也是您提到的禁止本地[]byte嵌入的标志。

我也希望教 go/types 了解 embed.Files 等的细微语义(即,它们只接受字符串文字参数),而不是教它关于 //go:embed。

至少只有字符串常量,甚至可以在 Go 类型系统中完成。

但是, go/types甚至需要知道嵌入文件, //go:embed吗? 毕竟,这些变量的类型非常清楚(除了本地的[]byte嵌入,如上所述)。

//go:embed "foo"
var x []byte

这真的意味着可变吗? 我想不出一个用例,我想嵌入一个静态资产,但在运行时改变它,所以我觉得这只会启用错误。 如果有什么东西试图改变它,我会更高兴地把它塞进.rodata并惊慌失措。

(由上面的x[0] = 'x'提示。)

@Merovius写道:

@mdempsky FSembed点导入之后是相同的类型。 所以,这种特殊情况似乎是一个直接的“是的,很好”(尝试过它已经有效)。

谢谢。 我忘记了点导入在编译器中是如何工作的,所以我误读了代码。 在包含该示例之前,我应该先尝试。

但是,我认为实现的语义现在很好 - 如果需要,我们总是可以在以后允许更多类型和/或别名和/或什至“相同的基础类型”。

[]byte[]uint8在 Go 规范下是相同的类型,无数其他使用类型别名构造的类型也是如此。 如果编译器将embed.Files("foo" + ".txt")embed.Files("foo.txt")区别对待是不可接受的,那么相同的参数应该扩展为不允许它以不同的方式对待别名类型。

但是, go/types甚至需要知道嵌入文件, //go:embed吗?

不,它不需要,就像它也不需要知道embed.Files的特殊语义一样。 但是对于任何一种语法, go/types 仍然可以警告滥用的价值。

——

@tv42写道:

这真的意味着可变吗?

显然。 Go 编译器专门安排stringembed.FS嵌入进行重复数据删除并放入 Rodata,而[]byte嵌入则不是:

https://github.com/golang/go/blob/56b783ad94f9a55163d494d5b34c783a9603d478/src/cmd/compile/internal/gc/embed.go#L224

但是,该提案似乎确实没有解决这一点。

感谢您的深思熟虑的评论。 我已经提交了两个问题来跟进:

#43216 - 删除对局部变量嵌入指令的支持
#43217 - 定义必须与 //go:embed 一起使用的字符串和字节类型别名

我没有为 //go:embed 语法本身提交问题。 我想在这里说明原因,这样每个人都不会觉得我只是在忽略这一点。

回到我写关于提案过程的博客时,我花了很长时间寻找(和不)处理大型团体决策的方法。 我发现对我来说很有意义的一个来源是 John Ousterhout 的“开放决策”帖子。 整篇文章值得一读,但我在这里只引用有关重新考虑的部分:

公开决策的最后一条规则是确保除非有重要的新信息,否则不要重新考虑决策。 这个规则有两个部分。 首先,您必须准备好纠正在重大方面被证明是错误的决定。 在初创公司中尤其如此:许多决策必须在没有完整信息的情况下做出,其中一些不可避免地会被证明是错误的。 一个迅速纠正的错误决定几乎不会造成损害,但一个不纠正的错误决定可能是灾难性的。

另一方面,您不应重新考虑某个决定,除非在做出最初的决定后出现了重要的新信息。 如果没有可用的新信息,则重新考虑决策可能会产生与原始决策相同的结果,从而浪费每个人的时间。 员工在做出决定后几周来找我并不罕见:“约翰,我投票反对 XYZ 的决定,我想得越多,我就越相信这是错误的;我真的认为我们需要重新考虑这个。” 我的回答是“你有什么新信息?” 如果答案是“无”,我们不会重新考虑。 在这种情况下,记录讨论期间提出的论点会很有帮助,这样您就可以验证新信息是否确实是新信息。 如果您太容易重新开始决策,您最终会在没有最终决定的情况下前后摇摆不定,员工会犹豫是否执行决定,因为他们不相信这些决定是永久性的。

如果您想确保不会重新考虑太多决策,请务必在决策过程中广泛收集意见。 如果你没有得到足够的输入,你就会增加做出决定后出现重要新输入的可能性,这意味着你必须重新考虑。 如果您在收集意见方面做得很好,那么您必须重新审视自己的决定的可能性就会大大降低。

这真的引起了我的共鸣:Go 提案流程(通常像大型开源项目一样)是一个系统,其提供的负载远高于工作容量,因此我们(如有必要不同意并)承诺并继续下一个决定是很重要的.

string 和 []byte 是在考虑这个问题的过程中很晚才添加的,以响应最初的反馈,我们显然没有考虑所有这些含义。 所以我提交了这两个新问题,#43216 和 #43217。

另一方面,//go:embed 语法是原始讨论的核心部分,并被广泛讨论,有利有弊。 我不认为有“重要的新信息”应该使我们完全重新考虑该语法,因此为了继续前进并牢记 Ousterhout 关于重新考虑的建议,我将其搁置一边。

再次感谢您指出字符串和 []byte 问题!

显然。 Go 编译器专门安排stringembed.FS嵌入被去重并放入rodata,而[]byte嵌入不是:

@rsc你有没有想过最后一点? 对于当前的实现,为了生成更好的二进制文件,使用 string 而不是 []byte 可能成为“最佳实践”。 我们同意吗?

我不明白为什么这会是“最佳实践”。 对我来说,这类似于说将哨兵错误设为常量是“最佳实践”,因此我们不应该允许类型为错误的包范围变量 - 因为我不同意这是一个好的实践,并且不同意将附加限制作为解决方案。

我可以看到只在本地变量中使用string的参数。 但是在包范围的变量中,语义清晰且定义明确,我不建议使用[]byte嵌入,而不是建议使用任何其他[]byte变量。

@mvdan ,如果人们倾向于使用string而不是[]byte作为最佳实践,我会认为这是一件好事。 string是“不可变字节串”的适当 Go 类型,而[]byte是“可变字节串”或“不确定可变性的字节串”的适当类型。

对于类型string (不可变)并希望将其用作类型[]byte (不确定)的值的情况,您已经可以使用unsafe正确执行此操作. (例如,参见我的unsafeslice.OfString )。 也许我们应该为该操作添加一个受支持的标准库,但这似乎是一个单独的提议。

因此,如果您确实打算将该值设为只读,则始终使用string类型似乎很好。

@Merovius @bcmills你提出了很好的观点,而且我不反对。 我只是想确保提案设计者在最终发布之前考虑这种区别。

我真的不认为重复数据删除在实践中会出现很多。 (在什么情况下多个包会嵌入完全相同的文件?)人们应该使用他们需要的形式,而不用担心“字符串意味着我的二进制文件更小”,因为总的来说我认为这不会是真的。

一些人询问了 ETag。 我们的时间不够了,但我已经在https://github.com/golang/go/issues/43223提交了一份提案,希望这会带来一个可以进入 Go 1.17 的好主意。 很抱歉无法在本轮中获得它。

感谢您提交 #43216 和 #43217。 如果这些被接受,他们将通过//go:embed提案实质性地解决我的悬而未决的问题。

另一方面,//go:embed 语法是原始讨论的核心部分,并被广泛讨论,有利有弊。 我不认为有“重要的新信息”应该使我们完全重新考虑该语法,因此为了继续前进并牢记 Ousterhout 关于重新考虑的建议,我将其搁置一边。

我可以尊重不想重新讨论已经通过广泛讨论决定的事情。 但是在回顾了#35950、 Reddit 线程和这里的讨论之后,我认为他们不能证明使用//go:embed的决定是合理的。

以下是我发现的有关指示要嵌入哪些文件的语法的评论:


18 个 GitHub 问题和 Reddit 评论

  • https://github.com/golang/go/issues/35950#issuecomment -561443566 “ //go:embed方法也引入了另一个级别的复杂性。您必须解析魔术注释才能对代码。“嵌入包”方法似乎对静态分析更友好。” (注意:修订后的//go:embed提案确实使键入检查代码变得更加容易,但是如果分析器想要实际查看使用的字符串,这仍然很重要,因为它们不在 go/ast 中。)
  • https://github.com/golang/go/issues/35950#issuecomment -561450136 “这是使用包的一个非常有力的论据。它还使其更具可读性和可记录性,因为我们可以使用常规 godoc 来记录所有内容,而不是而不是深入 cmd/go 的文档。” (注意:我认为这主要由 #43217 解决,因为然后包嵌入类型提供了一个简单的参考点。)
  • https://github.com/golang/go/issues/35950#issuecomment -561840094 “是否有原因它不能成为 go build/link 的一部分......例如go build -embed example=./path/example.txt和一些暴露的包访问它(例如embed.File("example") ,而不是使用go:embed ?”(建议函数语法超过//go:指令。被否决,但我怀疑是因为它建议了go build标志。)
  • https://github.com/golang/go/issues/35950#issuecomment -561726107 “我不是特别喜欢编译为代码的评论,但我也发现影响编译的伪包有点也很奇怪。如果不使用目录方法,也许将某种嵌入顶级声明实际内置到语言中可能更有意义。它的工作方式与导入类似,但仅支持本地路径和需要为它分配一个名称。” (不喜欢//go:或新的内置函数,但仍然更喜欢代码而不是注释。)
  • https://github.com/golang/go/issues/35950#issuecomment -561856033 “此外,由于//go:generate指令不会在 go build 时自动运行,所以 go build 的行为可能看起来有点不一致: //go:embed将自动工作,但对于//go:generate您必须手动运行 go generate。(如果它生成构建所需的 .go 文件,则//go:generate已经可以破坏 go get 流程)。 ” (包括完整性,但我们已经有//go:指令,即使没有//go:embed ,它们也会影响和不影响go build行为,所以我不觉得这个论点令人信服。)
  • https://github.com/golang/go/issues/35950#issuecomment -562005821 “我会投票支持新包而不是指令。更容易掌握,更容易处理/管理,更容易以记录和扩展。例如,您能否轻松找到诸如“go:generate”之类的 Go 指令的文档?“fmt”包的文档呢?您明白我要做什么了吗?“ (同样,主要由 #43217 解决。)
  • https://github.com/golang/go/issues/35950#issuecomment -562200553 "一个支持注释 pragma (//go:embed) 而不是特殊目录名称 (static/) 的参数:注释让我们在包(或 xtest 存档)的测试存档中嵌入一个文件,而不是被测库。注释只需要出现在 _test.go 文件中。” (注意:参数是针对特殊目录的,而不是专门针对//go:embed ;相同的参数扩展到embed.Files函数等)
  • https://github.com/golang/go/issues/35950#issuecomment -562235108 “我喜欢package embed使用 Go 语法的方法”
  • https://github.com/golang/go/issues/35950#issuecomment -562713786 “ import "C"方法已经为“神奇”导入路径开创了先例。IMO 效果很好。”
  • https://github.com/golang/go/issues/35950#issuecomment -562768318 “很多人谈到自动提供一个 API 来读取嵌入的文件,而不是需要另一个像魔术评论这样的信号。我认为应该是要走的路,因为它为该方法提供了一种熟悉的编程语法。使用一个特殊的包,可能是前面提到的runtime/embed ,将满足这一点,并且可以在未来轻松扩展。”
  • https://github.com/golang/go/issues/35950#issuecomment -562959330 “与其重用评论(最终散落在各处,而且就个人而言,只是感觉很恶心),[... ]。” (继续建议使用 go.mod 文件来枚举嵌入的文件。公平地说,还认为“这可以解决诸如必须将魔法包限制为仅允许字符串作为参数之类的问题,但确实意味着更难检查是否嵌入式资产实际上可以在任何地方使用,或者最终成为无用功。”)
  • https://github.com/golang/go/issues/35950#issuecomment -562966654 “还有一个想法:与其在 go.mod 中添加一种新的动词,我们可以引入一种新的包,一个数据包,以通常的方式在 go.mod 中导入和使用。这是一个稻草人草图。” (优先于代码而不是注释。)
  • https://github.com/golang/go/issues/35950#issuecomment -563156010 “这里还没有看到提到:Go 源代码处理工具(编译器、静态分析器)的 API 与运行时 API 一样重要. 这种 API 是 Go 的核心价值,有助于发展生态系统(如go/ast / go/formatgo mod edit )。 [...] 在这种情况下一个特殊的包,我认为go.mod解析( go mod工具)或go/ast解析器没有任何变化。”
  • https://github.com/golang/go/issues/35950#issuecomment -601748774 建议 go.res 资源文件列表。 使用代码,而不是注释。
  • https://old.reddit.com/r/golang/comments/hv96ny/qa_goembed_draft_design/fyv9gxw/ “为什么我们需要 //go:embed 注释,例如 binclude 做了binclude.Include(filename)来包含一个文件/目录是什么关于fs.Embed(filename) ?”
  • https://github.com/golang/go/issues/41191#issuecomment -686625127 “在我看来,在语言中添加语法糖来支持这种工具变化是一种语言变化。我相信这对其他人来说是显而易见的,但这实际上是注释即代码。我强烈觉得魔法/糖有损于语言的简单性和可读性;很容易错过嵌入文件的魔法注释。”
  • https://github.com/golang/go/issues/41191#issuecomment -690423900 “我和@tristanfisher有类似的担忧//go:embed可能更常见。也许是时候为编译器考虑不同的语法了指示?”
  • https://github.com/golang/go/issues/41191#issuecomment -690522509 “我同意你对魔术评论的担忧。”

当我阅读评论时,他们似乎总是喜欢 Go 语法,但要注意避免会破坏工具的更改。 我没有看到任何人主张//go:embed如拼写,尽可能只接受原样。 与更多的//go:指令相比,为遗留类型检查器添加具有向后兼容性存根的新编译器内在函数似乎更符合讨论参与者的偏好。

以 #43216 和 #43217 中的赞/不赞投票方式:

  • 如果您更喜欢新的编译器内在函数(例如CL 276835 ),请竖起大拇指 (

    import "embed"
    
    var website = embed.Files("logo.jpg", "static/*")
    

    有关更多使用示例,请参阅embed_test.go

    (注意:参数必须是字符串文字,而不仅仅是字符串值。也就是说,声明的字符串常量如const logo = "logo.jpg"或常量字符串表达式如"logo" + ".jpg"也是不允许的。然而, embed.Files等可以在任何上下文中使用,不仅用于初始化变量。)

  • 如果您更喜欢新的编译器指令(即当前的提案),请点赞 (👎):

    import "embed"
    
    //go:embed logo.jpg static/*
    var website embed.FS
    

@mdempsky我觉得 Russ 已经很清楚了,需要什么来证明向他撤销决定的合理性 - 新信息。 我认为收集以前的评论显然不是那样的。 无意冒犯。

这些新功能没有先例,对吧? 也就是说,看起来像一个普通的包函数但实际上是一种特殊的内置函数,只能以某种方式调用?

你说“内在”,但当前的内在函数的行为与普通的 Go 函数完全一样。

x := 10000
_ = bits.RotateLeft64(10, x)

将新指令装扮成看起来像 Go 函数,但与 Go 函数具有不同(更严格)的语法,从我坐的位置来看,这似乎是一个糟糕的选择。 我认为embed需要在规范中进行描述,一方面,与//go:指令不同。

(您可以通过创建一个采用非导出type internalString string参数的函数来近似普通 Go 代码中的“仅允许文字参数”,但我认为这不是您所提议的,因为您的 CL 对解析器和类型检查器。)

@Merovius根据 Go 提案流程:

共识与分歧

提案过程的目标是及时就结果达成普遍共识。

如果不能达成普遍共识,提案审查组通过审查和讨论该问题并在他们之间达成共识来决定下一步。 如果提案审查组之间甚至无法达成共识(这将是非常不寻常的),仲裁者 (rsc@) 将审查讨论并决定下一步。

在我阅读评论时,共识似乎有利于 Go 代码语法。 如果现在的共识实际上是坚持//go:embed ,我尊重这一点。 但我认为记录在案的过程并不能证明继续推进//go:embed的最初决定是合理的。

(目前,民意调查结果较新指令更偏向于新功能,但幅度不大。如果赞成的人数不超过反对的人数至少为 2:1,我可以放弃这一点。)

@cespare

这些新功能没有先例,对吧? 也就是说,看起来像一个普通的包函数但实际上是一种特殊的内置函数,只能以某种方式调用?

既有 Universe 的预声明函数,也有包 unsafe 的函数。

你可以争辩说 Go 规范中记录了包不安全,但我认为它不需要。 Go 过去支持用户无法使用包 unsafe 的模式,即使在今天,包 unsafe 的功能和限制也只记录在包文档中,而不是 Go 规范中。

Go 运行时中还有一些内部函数仅由 Go 编译器实现。 例如, getggetcallerpcgetcallerspgetclosureptr

(您可以通过创建一个采用非导出类型 internalString 字符串参数的函数来近似正常 Go 代码中的“仅允许文字参数”,

我认为这将是进一步缩小遗留类型检查器行为的合理补充。

但我认为这不是您的建议,因为您的 CL 对解析器和类型检查器进行了更改。)

CL 276835 不会更改解析器,只是删除了为//go:embed添加的新解析器代码。 它确实改变了类型检查器,但与之前的//go:embed

扩展 go/types 以了解包嵌入很容易,但我选择不为 CL 276835 专门表明它仍然有效(例如,cmd/vet 在包嵌入的单元测试中没有失败)。

@mdempsky您可能不同意当时是否达成共识。 就像你可能不同意决定本身一样。 不过,我认为这不会真正改变事情。 归根结底,“有共识”也是做出的决定。 关于需要新信息才能逆转的观点完全相同,也适用于决定。

需要满足每个人是一个 DDoS 向量——无论是关于决策还是制定过程。

FTR,已经讨论了关于工具与语言变化的考虑的论点-你可能会与决定的结果,或者与制造过程中不同意。

需要满足每个人是一个 DDoS 向量——无论是关于决策还是制定过程。

我并不要求我个人感到满意,而且我觉得你这样描述我的帖子是一种侮辱。 早些时候你还对我轻描淡写,好像我不熟悉 Go 语言或 Go 编译器。 请停止傲慢。

在上面列出了许多评论,人们几乎普遍表示倾向于添加新的//go:指令,而没有人肯定地评论支持它们。 因此,在我看来,社区压倒性的表达偏好似乎支持 Go 语法,而我的评论是在为他们陈述的偏好辩护。

然而,就目前而言, https: //github.com/golang/go/issues/41191#issuecomment -747095807 的赞许比赞许多。 这让我感到惊讶,因为它似乎与之前讨论中的所有评论不一致。 但我很高兴接受这个问题已经得到直接解决,并且(特别是作为将在 Go 编译器中长期支持此功能的人)我现在更高兴支持//go:embed看到这实际上是社群的偏好,而不仅仅是提案作者的偏好。 如果讨论被关闭,就不会达到这个结果,正如你似乎有意的那样。

FTR,已经讨论了关于工具与语言变化的

该评论与我提出的论点无关。 我在 CL 276835 中原型化的替代拼写与现有的//go:embed拼写具有完全相同的技术属性:需要知道要嵌入哪些文件的工具将需要更新以不同方式处理 Go 源文件(例如,错误关于滥用//go:embed注释或embed.Bytes内置函数),而现有工具可以继续合理地处理代码而无需担心它们(例如,go/types 将忽略//go:embed注释但不会检测它是否应用于不正确的变量类型,并且它可以使用存根声明对embed.Bytes进行类型检查,但它不会知道拒绝所有使用字符串文字以外的参数的调用)。

这些是否是“语言变化”的问题更具哲学性而不是技术性。

(Russ 在//go:embed提案下“程序是否有效不会改变”的论点也是错误的。https://github.com/golang/go/issues/41191#issuecomment-747799509 给出了一个例子一个根据 Go 规范过去并且有效的包,并且也被 Go 工具链通过 Go 1.15 版本接受,但在 Go 1.16 中将不再有效。)

作为对 Matt 使用var website = embed.Files("logo.jpg", "static/*")的提议给予 👍 的人,我对使用评论表单( //go:embed logo.jpg static/* )的关注是“易用性”。

例如,这 2 个程序示例将输出 2 个不同的东西,只是因为错过了一个“导入”:

import (
    "fmt"
)

//go:embed sample.txt
var sample string

func main() {
    fmt.Println(sample) // will print a blank line
}
import (
    "embed"
    "fmt"
)

//go:embed sample.txt
var sample string

func main() {
    fmt.Println(sample) // will print contents of sample.txt
}

通过强制开发人员通过语言语义使用导入,您可以最大限度地减少嵌入文件无法按预期工作的问题,因为它们没有按预期初始化。

@kushieda-minori 虽然我认为我的建议也更易于使用,但第一个示例已经无法编译:

./x.go:7:3: //go:embed only allowed in Go files that import "embed"

我认为 #43217 也进一步缓解了这个问题,因为您需要导入“嵌入”来声明embed.Bytesembed.String类型的变量。 如果将//go:embed指令应用于类型不正确的变量,编译器(但不是 go/types 或 cmd/vet)也会报告错误。

但是, @mdempsky意外地在旧版本的 Go 上编译不会失败,并且可能会产生嵌入工作的错误感觉。

旧版本的 Go 没有包嵌入,所以import "embed"会失败。 确实存在用户可以编写的风险:

package p

//go:embed foo.txt
var foo []byte

它将被 Go 1.15 及更早版本默默接受。 但它不会被 Go 1.16 和更新版本接受。 至少没有任何程序可以同时使用 Go 1.15 和 Go 1.16 编译并具有不同的语义(至少由于包嵌入)。

我认为这里适当的(部分)修复是让 cmd/compile 停止接受未知的//go:指令。 例如,当前提案相对于 Go 内置语法的另一个限制是,如果您输入错误//go:embed指令(比如//go:enbed//go;embed// go:embed有空格),它也会被默默地忽略。 (而像enbed.Bytes("foo.txt")这样的拼写错误会导致类型检查错误,即使是未修改的 go/types。)

关于未验证的go:指令的要点。 如果强制执行,这将有助于减轻难以发现的拼写错误。

我刚才的另一个想法是我的工具设置为根据需要自动添加/删除导入。 如果我的工具已经过时,我是否必须通过删除“未使用”的embed导入来对抗它? 我意识到如果我使用embed.String等就解决了,但是使用常规的[]byte]string应该是完全有效的。 如果他们没有看到特定的embed.*别名,那么这可能会让一个新的 gopher 感到沮丧,他们正在挑选网络片段来获得一些工作。

但是使用常规的[]byte]string应该是完全有效的。

如果 #43217 被接受,他们就不会。 我建议阅读 #43216 和 #43217。 到目前为止,他们都得到了压倒性的积极支持,而且似乎很可能被我接受。 (虽然我不是提案审查委员会的成员。)

谢谢,第一次通读#43217的时候,漏了一个关键词
“拥有”用于使用embed.*类型。

我想我唯一关心的就是你指出的最后一个。

2020 年 12 月 17 日,星期四,20:24 Matthew Dempsky通知@github.com
写道:

但是使用常规的 []byte] 和 string 应该是完全有效的。

如果 #43217 https://github.com/golang/go/issues/43217是,他们就不会
公认。 我建议阅读 #43216
https://github.com/golang/go/issues/43216和 #43217
https://github.com/golang/go/issues/43217 。 他们都收到了
到目前为止获得压倒性的积极支持,并且似乎很可能被接受
对我来说。 (虽然我不是提案审查委员会的成员。)


你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/golang/go/issues/41191#issuecomment-747808153 ,或
退订
https://github.com/notifications/unsubscribe-auth/ADLZABJWBJX475BVYDVD6ODSVKVOTANCNFSM4QTHVTUA
.

此页面是否有帮助?
0 / 5 - 0 等级