Go: 提案:cmd/go:支持在二进制文件中嵌入静态资产(文件)

创建于 2019-12-04  ·  176评论  ·  资料来源: golang/go

有许多工具可以将静态资产文件嵌入到二进制文件中:

实际上, https :

提议

我认为是时候做好一次并减少重复,添加官方支持将文件资源嵌入到 cmd/go 工具中。

现状问题:

  • 工具太多
  • 使用基于 go:generate 的解决方案会使用每个文件的第二个(稍大一些)副本来膨胀 git 历史记录。
  • 不使用 go:generate 意味着不能使用go install或让人们编写自己的 Makefile 等。

目标:

  • 不要签入生成的文件
  • 根本不生成 *.go 文件(至少不在用户的工作区中)
  • 使go install / go build自动嵌入
  • 让用户为每个文件/glob 选择需要哪种访问类型(例如 []byte, func() io.Reader , io.ReaderAt等)
  • 也许在适当的地方存储压缩在二进制文件中的资产(例如,如果用户只需要io.Reader )? (编辑:但可能不是;见下面的评论)
  • 编译时不执行代码; 这是一项长期存在的围棋政策。 go buildgo install不能运行任意代码,就像go:generate在安装时不会自动运行一样。

两种主要的实现方法是//go:embed Logo logo.jpg或一个众所周知的包( var Logo = embed.File("logo.jpg") )。

去:嵌入方法

对于go:embed方法,人们可能会说任何go/build -selected *.go文件都可以包含以下内容:

//go:embed Logo logo.jpg

例如,编译为:

func Logo() *io.SectionReader

(向io包添加依赖项)

或者:

//go:embedglob Assets assets/*.css assets/*.js

编译成,说:

var Assets interface{
     Files() []string
     Open func(name string) *io.SectionReader
} = runtime.EmbedAsset(123)

显然,这还没有完全充实。 压缩文件也需要一些东西,只产生一个io.Reader

嵌入包方法

另一种高级方法是没有神奇的//go:embed语法,而是让用户在一些新的"embed""golang.org/x/foo/embed"包中编写 Go 代码:

var Static = embed.Dir("static")
var Logo = embed.File("images/logo.jpg")
var Words = embed.CompressedReader("dict/words")

然后让 cmd/go 识别对 embed.Foo("foo/*.js") 等的调用,并让 glob 在 cmd/go 中完成工作,而不是在运行时。 或者,某些构建标记或标志可能会使其回退到运行时做事。 Perkeep(上面链接)有这样一种模式,它可以很好地加速增量开发,你不关心链接一个大的二进制文件。

顾虑

  • 选择一种风格(//go:embed* vs a magic package)。
  • 阻止某些文件?

    • 可能块嵌入../../../../../../../../../../etc/shadow

    • 也许也阻止进入.git

Proposal Proposal-Hold

最有用的评论

@robpike和我在几年前(在有提案流程之前)讨论了一项关于这样做的提案,并且再也没有做任何事情。 多年来一直困扰着我,我们从未完成过那件事。 我记得的想法是只有一个特殊的目录名称,如包含静态数据的“静态”,并自动通过 API 使它们可用,无需注释。

我不相信“压缩与非压缩”旋钮的复杂性。 如果我们这样做,那么人们会希望我们添加对压缩方式、压缩级别等的控制。 我们需要添加的只是嵌入纯字节文件的能力。 如果用户想将压缩数据存储在该文件中,那太好了,细节由他们决定,Go 方面根本不需要 API。

所有176条评论

值得考虑embedglob是否应该支持完整的文件树,也许使用某些 Unix shell 支持的**语法。

有些人需要能够使用http.FileServer使用 HTTP 为嵌入式资产提供服务。

我个人使用 mjibson/esc(这样做)或在某些情况下我自己的文件嵌入实现,它重命名文件以创建唯一路径并添加从原始路径到新路径的映射,例如"/js/bootstrap.min.js": "/js/bootstrap.min.827ccb0eea8a706c4c34a16891f84e7b.js" 。 然后您可以在模板中使用此地图,如下所示: href="{{ static_path "/css/bootstrap.min.css" }}"

我认为这样做的结果是找出构建程序所需的文件将是非常重要的。

//go:embed方法也引入了另一个级别的复杂性。 您必须解析魔术注释才能对代码进行类型检查。 “嵌入包”方法似乎对静态分析更友好。

(只是在这里大声沉思。)

@opennota

需要能够使用http.FileServer使用 HTTP 为嵌入式资产提供服务。

是的,上面的第一个链接是我写的一个包(在 2011 年,在 Go 1 之前)并且仍在使用,它支持使用 http.FileServer: https ://godoc.org/perkeep.org/pkg/fileembed#Files.Open

@cespare ,

//go:embed 方法也引入了另一个级别的复杂性。 您必须解析魔术注释才能对代码进行类型检查。 “嵌入包”方法似乎对静态分析更友好。

是的,好点。 这是使用包的一个非常有力的论据。 它还使其更具可读性和可记录性,因为我们可以使用常规 godoc 来记录所有内容,而不是深入到 cmd/go 的文档中。

@bradfitz - 你想关闭这个https://github.com/golang/go/issues/3035吗?

@agnivade ,感谢您找到它! 我以为我记得,但找不到。 让我们暂时打开它,看看其他人的想法。

如果我们使用魔法包,我们可以使用未导出的类型技巧来确保调用者将编译时常量作为参数传递: https :

(这是这里引用的策略:https://groups.google.com/forum/#!topic/golang-nuts/RDA9Hag8RZw/discussion)

我担心的一个问题是它如何处理太大而无法放入内存的单个或所有资产,以及是否可能有一个构建标签或每个文件访问选项,以在优先考虑访问时间与内存占用或一些中间实现之间进行选择。

我解决这个问题的方法(因为当然我也有自己的实现:))是提供一个 http.FileSystem 实现来服务所有嵌入式资产。 这样,您就不必依赖魔术注释来安抚类型检查器,资产可以很容易地由 http 提供服务,可以提供用于开发目的的回退实现(http.Dir)而无需更改代码,最后实现非常通用,因为 http.FileSystem 涵盖了很多,不仅在读取文件方面,而且在列出目录方面。

人们仍然可以使用魔术注释或其他任何东西来指定需要嵌入的内容,尽管通过纯文本文件指定所有 glob 可能更容易。

@AlexRouSg此提议仅适用于适合直接包含在最终可执行文件中的文件。 对于太大而无法放入内存的文件,不适合使用它。 没有理由让这个工具复杂化来处理这种情况; 对于这种情况,请不要使用此工具。

@ianlancetaylor ,我认为@AlexRouSg 所做的区别在于将文件作为全局[]byte s(不可分页,可能可写的内存)提供与提供 ELF 部分的只读、按需视图之间通常存在于磁盘上(在可执行文件中),例如通过Open调用返回*io.SectionReader 。 (我不想将http.Filehttp.FileSystem放入 cmd/go 或 runtime...net/http 可以提供适配器。)

@bradfitz http.File 本身是一个接口,与http包没有技术依赖关系。 任何Open方法提供符合该接口的实现可能是一个好主意,因为StatReaddir方法对于此类资产都非常有用

@urandom ,它无法实现 http.FileSystem,但是,如果不引用“http.File”名称(https://play.golang.org/p/-r3KjG1Gp-8)。

@robpike和我在几年前(在有提案流程之前)讨论了一项关于这样做的提案,并且再也没有做任何事情。 多年来一直困扰着我,我们从未完成过那件事。 我记得的想法是只有一个特殊的目录名称,如包含静态数据的“静态”,并自动通过 API 使它们可用,无需注释。

我不相信“压缩与非压缩”旋钮的复杂性。 如果我们这样做,那么人们会希望我们添加对压缩方式、压缩级别等的控制。 我们需要添加的只是嵌入纯字节文件的能力。 如果用户想将压缩数据存储在该文件中,那太好了,细节由他们决定,Go 方面根本不需要 API。

一些想法:

  • 应该不可能在执行嵌入的模块之外嵌入任何文件。 我们需要在创建文件时确保文件是模块 zip 文件的一部分,这也意味着没有符号链接、大小写冲突等。我们无法在不破坏总和的情况下更改生成 zip 文件的算法。
  • 我认为将嵌入限制在同一目录(如果使用//go:embed注释)或特定子目录(如果使用static )更简单。 这使得理解包和嵌入文件之间的关系变得更加容易。

无论哪种方式,这都会阻止嵌入/etc/shadow.git 。 两者都不能包含在模块 zip 中。

一般来说,我担心过多地扩展 go 命令的范围。 然而,这个问题有这么多解决方案这一事实意味着可能应该有一个官方解决方案。

我熟悉go_embed_datago-bindata (其中有几个分支),这似乎涵盖了这些用例。 是否有其他人解决的重要问题没有涵盖?

阻止某些文件应该不会太难,尤其是当您使用staticembed目录时。 符号链接可能会使这有点复杂,但您可以阻止它嵌入当前模块之外的任何内容,或者,如果您在 GOPATH 上,则在包含目录的包之外。

我不是特别喜欢编译为代码的评论,但我也发现影响编译的伪包也有点奇怪。 如果不使用目录方法,也许在语言中实际内置某种embed顶级声明可能更有意义。 它的工作方式与import类似,但仅支持本地路径,并且需要为其分配名称。 例如,

embed ui "./ui/build"

func main() {
  file, err := ui.Open("version.txt")
  if err != nil {
    panic(err)
  }
  version, err = ioutil.ReadAll(file)
  if err != nil {
    panic(err)
  }
  file.Close()

  log.Printf("UI Version: %s\n", bytes.TrimSpace(version))
  http.ListenAndServe(":8080", http.EmbeddedDir(ui))
}

编辑:你打败了我,@jayconrod。

要扩展https://github.com/golang/go/issues/35950#issuecomment -561703346,有一个关于公开 API 的难题。 公开数据的明显方法是[]bytestringRead -ish 接口。

典型的情况是您希望嵌入的数据是不可变的。 但是,所有暴露[]byte (包括io.Readerio.SectionReader等)的接口必须(1)制作副本,(2)允许可变性,或(3)尽管是[]byte但不可变。 将数据公开为string s 解决了这个问题,但代价是 API 往往最终需要复制,因为大量使用嵌入文件的代码最终需要以一种或另一种方式进行字节切片。

我建议路线 (3): 尽管是[]byte但保持不变。 您可以通过为后备数组使用只读符号来廉价地强制执行此操作。 这还可以让您安全地公开与[]bytestring相同的数据; 试图改变数据将失败。 编译器无法利用不变性,但这并没有太大的损失。 这是工具链支持可以带来的东西(据我所知)现有的代码生成包都没有。

(第三方代码生成包可以通过生成包含标记为只读的DATA符号的通用程序集文件,然后以string s 和[]byte s。我专门针对这个用例编写了CL 163747 ,但从未考虑将其集成到任何代码生成包中。)

我不确定你在谈论不变性方面的内容。 io.Reader已经强制执行不变性。 这就是重点。 当您调用Read(buf) ,它会将数据复制到 _you_ 提供的缓冲区中。 之后更改bufio.Reader的内部结构的影响为零。

我同意@DeedleFake。 我不想玩带有魔法[]byte数组支持的游戏。 可以从二进制文件复制到用户提供的缓冲区中。

这里还有一个问题——我有一个不同的项目,它使用 DTrace 源代码(嵌入式)。 这对\n 和\r\n 之间的差异很敏感。 (我们可以争论这在 DTrace 中是否是一件愚蠢的事情——这无关紧要,这就是今天的情况。)

无论它们如何出现在源代码中,反引号字符串都将它们视为 \n 是非常有用的,我依靠它与 go-generate 来嵌入 DTrace。

因此,如果在 go 命令中添加了一个嵌入文件,我会温和地建议更改 CR/CRLF 处理的选项可能会派上用场,特别是对于可能在不同系统上开发的人来说,默认行尾可以是一个问题。

与压缩一样,我真的很想停在“将文件字节复制到二进制文件中”。 CR/CRLF 规范化、Unicode 规范化、gofmt'ing,所有这些都属于别处。 签入包含您想要的确切字节的文件。 (如果您的版本控制不能让它们单独存在,可以检查 gzip 压缩的内容并在运行时对其进行 gunzip。)我们可以想象添加_许多_文件处理旋钮。 让我们停在 0。

引入一个新的保留目录名称可能为时已晚,正如我所愿。
(在 2014 年还不算太晚,但现在可能已经太晚了。)
因此,可能需要某种选择加入的评论。

假设我们定义了一个类型 runtime.Files。 然后你可以想象写:

//go:embed *.html (or static/* etc)
var files runtime.Files

然后在运行时您只需调用 files.Open 以获取带有数据的interface { io.ReadSeeker; io.ReaderAt } 。 请注意,var 未导出,因此一个包无法在另一个包的嵌入文件中进行抓取。

名称待定,但就机制而言,似乎应该足够了,我不知道如何使它更简单。 (当然欢迎简化!)

无论我们做什么,都需要能够支持 Bazel 和 Gazelle。 这意味着让 Gazelle 识别注释并写出一个 Bazel 规则来说明 globs,然后我们需要公开一个工具(go tool embedgen 或其他)来生成额外的文件以包含在构建中(go 命令将自动执行此操作,从不实际显示额外文件)。 这似乎很简单。

如果各种改造都不起作用,那么这就是反对使用这种新设施的论据。 这对我来说不是障碍——我可以像我一直在做的那样使用 go generate,但这意味着我无法从新功能中受益。

关于一般的 munging - 我可以想象一个解决方案,其中有人提供接口的实现(一侧类似于 Reader() ,另一侧是接收文件的东西 - 可能使用 io.Reader 实例化)从文件本身) - go cmd 将构建并运行以在嵌入之前对文件进行预过滤。 然后人们可以提供他们想要的任何过滤器。 我想有些人会提供准标准过滤器,如 dos2unix 实现、压缩等。(也许它们甚至应该是可链接的。)

我想必须假设无论嵌入式处理器是什么,它都必须可以在每个构建系统上编译,因为 go 会为此目的构建一个临时的本机工具。

引入一个新的保留目录名称可能为时已晚,正如我所愿。 [...] 可能需要某种选择加入的评论。

如果文件只能通过一个特殊的包访问,比如runtime/embed ,那么导入该包可能是选择加入的信号。

io.Read方法似乎可以为概念上简单的线性操作(如strings.Contains (例如cmd/go/internal/cfg )或,关键是template.Parse

对于这些用例,允许调用者选择是否将整个 blob 视为(可能是内存映射的) stringio.ReaderAt似乎是理想的。

不过,这似乎与一般的runtime.Files方法兼容:从runtime.Files.Open返回的东西可能有一个ReadString() string方法,该方法返回内存映射表示。

可能需要某种选择加入的评论。

我们可以使用go.mod文件中的go版本来做到这一点。 在1.15 (或其他)之前, static子目录将包含一个包,而在1.15或更高位置,它将包含嵌入的资产。

(不过,这在GOPATH模式下并没有真正的帮助。)

我不相信“压缩与非压缩”旋钮的复杂性。 如果我们这样做,那么人们会希望我们添加对压缩方式、压缩级别等的控制。 我们需要添加的只是嵌入纯字节文件的能力。

虽然我很欣赏简单的驱动力,但我们还应该确保我们满足用户的需求。

https://tech.townsourced.com/post/embedding-static-files-in-go/#comparison 中列出的 14 个工具中有 12 个支持压缩,这表明这是一个非常普遍的要求。

确实,可以将压缩作为预构建步骤之外的步骤进行,但这仍然需要 1) 进行压缩的工具 2) 检查某种assets.zip blob 到 vcs 3) 可能是一个实用程序嵌入 api 周围的库以撤消压缩。 目前还不清楚到底有什么好处。

最初提案中列出的三个目标是:

  • 不要签入生成的文件
  • make go install / go build 自动嵌入
  • 在适当的地方存储压缩在二进制文件中的资产

如果我们将其中的第二个理解为“不需要单独的嵌入工具”,那么不直接或间接支持压缩文件就无法满足所有这三个目标。

这是否需要包级别? 模块级别似乎是更好的粒度,因为很可能一个模块 = 一个项目。

由于此目录不包含 Go 代码†,它可能类似于_static吗?

† 或者,如果是,它将被视为名称恰好以“.go”结尾的任意字节,而不是要编译的 Go 代码

如果它是一个特殊的目录,那么逻辑就可以包含该目录树中的所有内容。 魔术嵌入包可以让您执行类似embed.Open("img/logo.svg")来打开资产树子目录中的文件。

字符串似乎足够好。 它们可以轻松地复制到[]byte或转换为Reader 。 代码生成或库可用于提供更高级的 API 并在init期间处理事情。 这可能包括解压或创建http.FileSystem

Windows 没有用于嵌入资产的特殊格式。 在构建 Windows 可执行文件时应该使用它吗? 如果是这样,这对可以提供的操作类型有任何影响吗?

不要忘记 gitfs 😂

是否有原因它不能成为 go build / link 的一部分......例如go build -embed example=./path/example.txt和一些公开访问它的包(例如embed.File("example") ,而不是使用go:embed ?

你需要在你的代码中存根

@egonelbre go build -embed是所有用户都需要正确使用它。 这需要完全透明和自动; 现有的go installgo get命令不能停止做正确的事情。

@bradfitz我会推荐https://github.com/markbates/pkr 而不是 Packr。 它使用标准库 API 来处理文件。

func run() error {
    f, err := pkger.Open("/public/index.html")
    if err != nil {
        return err
    }
    defer f.Close()

    info, err := f.Stat()
    if err != nil {
        return err
    }

    fmt.Println("Name: ", info.Name())
    fmt.Println("Size: ", info.Size())
    fmt.Println("Mode: ", info.Mode())
    fmt.Println("ModTime: ", info.ModTime())

    if _, err := io.Copy(os.Stdout, f); err != nil {
        return err
    }
    return nil
}

或者,某些构建标记或标志可能会使其回退到运行时做事。 Perkeep(上面链接)有这样一种模式,它可以很好地加速增量开发,你不关心链接一个大的二进制文件。

mjibson/esc 也这样做,在开发 web 应用程序时,这是一个很大的生活质量改进; 您不仅可以节省链接时间,还可以避免重新启动应用程序,这可能需要大量时间和/或需要重复额外的步骤来测试您的更改,具体取决于 web 应用程序的实现。

现状问题:

  • 使用基于 go:generate 的解决方案会使用每个文件的第二个(稍大一些)副本来膨胀 git 历史记录。

目标:

  • 不要签入生成的文件

好吧,这部分很容易解决,只需将生成的文件添加到.gitignore文件或等效文件中即可。 我一直都是这样做的...

因此,或者 Go 可以拥有自己的“官方”嵌入工具,默认情况下在go build上运行,并要求人们忽略这些文件作为约定。 那将是可用的不太神奇的解决方案(并且与现有的 Go 版本向后兼容)。

我只是在这里集思广益/大声思考……但实际上我总体上喜欢提议的想法。 🙂

此外,由于//go:generate指令不会自动运行go build的行为go build似乎有点矛盾: //go:embed会自动工作,但为//go:generate你必须手动运行go generate 。 (如果//go:generate生成构建所需的.go文件,它已经可以破坏go get流)。

//go:generate生成构建所需的.go文件,则它已经可以破坏 go get 流程

我认为通常的流程,我通常使用的流程,虽然需要一点时间来习惯,是将go generate完全用作开发端工具,只需提交文件它生成。

@bradfitz它不需要自己实现http.FileSystem 。 如果实现提供了实现http.File ,那么对于任何人来说都是微不足道的,包括 stdlib http 包提供Open函数的包装器,将类型转换为http.File为了符合http.FileSystem

不过,@andreynering //go:generate//go:embed非常不同。 这种机制可以在构建时无缝发生,因为它不会运行任意代码。 我相信这类似于 cgo 如何生成代码作为go build

我不相信“压缩与非压缩”旋钮的复杂性。 如果我们这样做,那么人们会希望我们添加对压缩方式、压缩级别等的控制。 我们需要添加的只是嵌入纯字节文件的能力。

虽然我很欣赏简单的驱动力,但我们还应该确保我们满足用户的需求。

https://tech.townsourced.com/post/embedding-static-files-in-go/#comparison 中列出的 14 个工具中有 12 个支持压缩,这表明这是一个非常普遍的要求。

我不确定我是否同意这个推理。

其他库所做的压缩与将其添加到本提案不同,因为它们不会降低后续构建的性能,因为替代方案通常是在构建之前而不是在构建期间生成。

与其他语言相比,低构建时间是一个明显的附加价值,并且压缩交换 CPU 时间以减少存储/传输占用空间。 如果很多 Go 包开始在go build上运行压缩,我们将增加更多的构建时间,而不是在构建期间简单地复制资产所增加的时间。 由于其他人这样做,我对添加压缩持怀疑态度。 只要最初的设计不通过设计阻止未来的扩展,增加对压缩的支持,把它放在那里,因为它可能会使某些人受益,这似乎是不必要的对冲。

文件嵌入并不是没有压缩就没有用,压缩是一种很好的方法,可以将二进制大小从 100MB 减少到 50MB——这很好,但对于我能想到的大多数应用程序的功能来说也不是一个明确的破坏者. 尤其是如果大多数“较重”的资产是诸如 JPEG 或 PNG 之类的文件,它们已经被很好地压缩了。

如果很多人实际上错过了压缩,现在保持压缩并添加它怎么样? (并且可以在没有不必要的成本的情况下完成)

添加到@sakjur上面的评论:压缩对我来说似乎是正交的。 我通常想压缩整个二进制文件或发布档案,而不仅仅是资产。 特别是当 Go 中的 Go 二进制文件很容易在没有任何资产的情况下达到数十兆字节时。

@mvdan我想我的一个担忧是,当我看到嵌入与其他一些预处理在一起时,经常会出现:缩小、打字稿编译、数据压缩、图像处理、图像大小调整、精灵表。 唯一的例外是仅使用html/template 。 因此,最终,您可能最终会使用某种“Makefile”或上传预处理的内容。 从这个意义上说,我认为命令行标志比注释更适合与其他工具一起使用。

我想我的一个担忧是,当我看到嵌入与其他一些预处理在一起时,经常会出现:缩小、打字稿编译、数据压缩、图像处理、图像大小调整、精灵表。 唯一的例外是仅使用 html/模板的网站。

谢谢,这是一个有用的数据点。 也许对压缩的需求并不像看起来那么普遍。 如果是这种情况,我同意将其排除在外是有意义的。

文件嵌入并不是没有压缩就没有用,压缩是一种很好的方法,可以将二进制大小从 100MB 减少到 50MB——这很好,但对于我能想到的大多数应用程序的功能来说也不是一个明确的破坏者.

二进制大小对于许多 Go 开发人员来说是一件大事(https://github.com/golang/go/issues/6853)。 Go 专门压缩 DWARF 调试信息以减少二进制文件的大小,即使这会以链接时间为代价(https://github.com/golang/go/issues/11799,https://github.com/golang/go/问题/26074)。 如果有一种简单的方法可以将二进制文件大小减半,我认为开发人员会抓住这个机会(尽管我怀疑这里的收益是否会如此显着)。

不过,这在 GOPATH 模式下并没有真正的帮助

也许,如果您处于 GOPATH 模式,此功能根本不适用,因为我想 Go 团队不打算永远为 GOPATH 进行功能奇偶校验? 已经存在 GOPATH 不支持的功能(例如带有校验和数据库的安全性、通过代理服务器下载依赖项以及语义导入版本控制)

正如@bcmills提到的,在 go.mod 文件中包含静态目录名称是在 Go 1.15 中引入此功能的好方法,因为可以在具有 <=go1.14 子句的 go.mod 文件中自动关闭该功能。

也就是说,这也意味着用户必须手动编写静态目录路径是什么。

我认为 vendor 目录和 _test.go 约定是很好的例子,说明他们如何使用 Go 和这两个功能更容易。

我不记得有多少人要求选择自定义供应商目录名称或能够将_test.go约定更改为其他内容。 但是,如果 Go 永远不会引入 _test.go 功能,那么今天在 Go 中进行的测试看起来会大不相同。

因此,也许比static更不通用的名称提供了更好的非冲突机会,因此与神奇的注释相比,拥有传统目录(类似于供应商和 _test.go)可能是更好的用户体验。

潜在低冲突名称的示例:

  • _embed - 遵循_test.go约定
  • go_binary_assets
  • .gobin遵循 .git 约定
  • runtime_files - 使其匹配runtime.Files结构

最后,在 Go 1.5 中添加了vendor目录。 Sooo,也许现在添加一个新约定并没有那么糟糕? 😅

我认为它应该公开一个 mmap-readonly []byte 。 仅对可执行文件中的页面进行原始访问,根据需要由操作系统分页。 除了bytes.NewReader ,还可以提供其他所有内容。

如果由于某种原因这是不可接受的,请提供ReaderAt而不仅仅是ReadSeeker ; 从前者构建后者是微不足道的,但另一种方式则没有那么好:它需要一个互斥锁来保护单个偏移量,并破坏性能。

文件嵌入并不是没有压缩就没有用,压缩是一种很好的方法,可以将二进制大小从 100MB 减少到 50MB——这很好,但对于我能想到的大多数应用程序的功能来说也不是一个明确的破坏者.

对于许多 Go 开发人员来说,二进制大小是一个大问题(#6853)。 Go 专门压缩 DWARF 调试信息以减少二进制文件的大小,即使这是以链接时间为代价的(#11799,#26074)。 如果有一种简单的方法可以将二进制文件大小减半,我认为开发人员会抓住这个机会(尽管我怀疑这里的收益是否会如此显着)。

这绝对是一个公平的观点,我可以看到我的论点如何被视为支持粗心大意的论据。 这不是我的意图。 我的观点更符合在不压缩的情况下发布此功能,这对某些人仍然有用,并且他们可以提供有关如何以长期感觉正确的方式正确添加压缩的有用反馈和见解。 资产可能会以调试信息不​​太可能发生的方式膨胀,并且如果实现使其易于执行,则其他人安装/导入的包的开发人员更容易不必要地降低构建性能。

另一种选择是将资产压缩作为构建标志,并将构建大小和时间之间的折衷留给构建者而不是开发者。 这将使决策更接近二进制的最终用户,他们可以决定压缩是否值得。 Otoh,这可能会为开发和生产之间的差异创造更大的表面积,因此它并不是比其他任何方法都更好的明确方法,而且我觉得这不是我想要提倡的。

我当前的资产嵌入工具在使用-tags dev构建时从资产文件加载内容。 一些像这样的约定在这里也可能有用; 例如,在摆弄 HTML 或模板时,它显着缩短了开发周期。

如果没有,调用者将不得不用一些*_dev.go*_nodev.go包装器包装这个较低级别的机制,并为dev场景实现非嵌入加载。 甚至不难,但这条路只会导致类似的工具爆炸,就像关于这个问题的第一个评论所描述的那样。 这些工具将不得不比今天做得更少,但它们仍然会成倍增加。

我认为-tags dev在 Go 模块之外运行时失败是合理的(无法弄清楚从哪里加载资产)。

一个go tool embed接受输入并以计算机识别的特殊格式生成 Go 输出文件,作为嵌入文件,然后可以通过runtime/emved或其他东西访问这些文件。 然后你可以做一个简单的//go:generate gzip -o - static.txt | go tool embed -o static.go

当然,一个很大的缺点是您必须提交生成的文件。

@DeedleFake这个问题始于

使用基于 go:generate 的解决方案会使用每个文件的第二个(稍大一些)副本来膨胀 git 历史记录。

哎呀。 没关系。 对不起。

文件嵌入并不是没有压缩就没有用,压缩是一种很好的方法,可以将二进制大小从 100MB 减少到 50MB——这很好,但对于我能想到的大多数应用程序的功能来说也不是一个明确的破坏者.

对于许多 Go 开发人员来说,二进制大小是一个大问题(#6853)。 Go 专门压缩 DWARF 调试信息以减少二进制文件的大小,即使这是以链接时间为代价的(#11799,#26074)。 如果有一种简单的方法可以将二进制文件大小减半,我认为开发人员会抓住这个机会(尽管我怀疑这里的收益是否会如此显着)。

如果需要它,那么人们将提交和嵌入压缩数据,并且将有包在runtime.Embed和执行内联解压的最终消费者之间提供一个层。

然后一两年后会有一个关于添加压缩​​的新问题,然后可以对其进行排序。

当我写goembed时,我说这是15 个相互竞争的标准之一:)

@tv42写道:

我认为它应该公开一个 mmap-readonly []byte 。 仅对可执行文件中的页面进行原始访问,根据需要由操作系统分页。

这条评论很容易被遗漏,而且非常有价值。

@tv42

我认为它应该公开一个 mmap-readonly []byte。 仅对可执行文件中的页面进行原始访问,根据需要由操作系统分页。 其他一切都可以在此之上提供,只需要 bytes.NewReader。

已经是只读的类型是string 。 另外:它提供了一个大小,与io.ReaderAt ,它不依赖于标准库。 这可能就是我们想要暴露的。

已经是只读的类型是string

但是Write等的整个生态系统都适用于[]byte 。 这是简单的实用主义。 我不认为 readonly 属性比io.Writer.Write文档说的更多问题

写入不得修改切片数据,即使是临时修改。

另一个潜在的缺点是,当用go:generate嵌入目录时,我可以检查git diff的输出并查看是否有任何文件错误存在。 有了这个建议 - ? 也许 go 命令会打印它嵌入的文件列表?

@tv42

但是 Write 等的整个生态系统都在 []byte 上工作。

html/template可以处理字符串。

Go 已经让你使用 -ldflags -X 来设置一些字符串(用于设置 git 版本、编译时间、服务器、用户等),这个机制可以扩展到设置 io.Readers 而不是字符串吗?

@bradfitz您是否提议在此处对非文本数据使用字符串? 嵌入像图标和小图像等小的二进制文件是很常见的。

@tv42你说的是Write但我想你的意思是Read 。 您可以使用strings.NewReaderstring转换为io.ReaderAt strings.NewReader ,因此使用字符串似乎不是障碍。

@andreynering string可以保存任何字节序列。

string可以保存任何字节序列。

是的,但它的主要目的是保存文本,而不是任意数据。 我想这可能会引起一些混乱,特别是对于没有经验的 Go 开发人员。

不过,我完全明白了。 谢谢澄清。

@ianlancetaylor

Read应该改变传入的切片。 Write不是。 因此Write文档说这是不允许的。 除了记录用户不得写入返回的[]byte之外,我认为没有什么需要的

仅仅因为strings.Reader存在并不意味着io.WriteString会找到编写字符串的有效实现。 例如, TCPConn没有WriteString

我不想让 Go 包含这样一个新功能,只是为了强制复制所有数据,只是为了将其写入套接字。

此外,一般假设字符串是人类可打印的,而[]byte通常不是。 将 JPEG 放在字符串中会导致很多乱七八糟的终端。

@opennota

不过,html/template 可以处理字符串。

是的,这很奇怪,它只按路径名接收文件,而不是读取器。 两个回应:

  1. 没有理由嵌入的数据不能同时具有Bytes() []byteString() string两种方法。

  2. 希望您不是每次请求都解析模板; 而对于每个请求它的请求,您确实必须将 JPEG 的数据发送到 TCP 套接字中。

@tv42我们可以根据需要添加WriteString方法。

我认为此功能的最常见用途不是写入未修改的数据,因此我认为我们不应该针对这种情况进行优化。

我不认为这个功能最常见的用途是写入未修改的数据,

我认为此功能最常见的用途是提供未修改的 Web 资产、图像/js/css。

但是不要相信我的话,让我们看看 Brad 的 fileembed 的一些进口商:

#fileembed pattern .+\.(js|css|html|png|svg|js.map)$
#fileembed pattern .*\.png



md5-f8b48fccd03599094034bf2b507e9e67



#fileembed pattern .*\.js$

等等..

对于轶事数据:我知道如果实现了这一点,我会立即在工作中的两个地方使用它,并且都将提供对静态文本文件的未经修改的访问。 现在我们使用//go:generate步骤将文件转换为常量(十六进制格式)字符串。

我会投票支持新的包而不是指令。 更容易掌握,更容易处理/管理,更容易记录和扩展。 例如,您能否轻松找到诸如“go:generate”之类的 Go 指令的文档? “fmt”包的文档呢? 你明白我要去哪里了吗?

因此,或者 Go 可以拥有自己的“官方”嵌入工具,该工具默认在go build

@andreynering我知道其他包管理器和语言工具允许这样做,但是在构建时运行任意代码/命令是一个安全漏洞(我希望这是显而易见的原因)。

在考虑此功能时,我想到了另外两件事:

  • 嵌入文件如何自动与构建缓存一起工作?
  • 它会阻止可重复的构建吗? 如果数据以任何方式更改(例如压缩它),则应考虑可重复性。

在第一条评论中链接的stuffbin主要是为了让自托管的 Web 应用程序嵌入静态(HTML、JS ...)资产。 这似乎是一个常见的用例。

除了编译/压缩讨论之外,另一个痛点是 stdlib 中缺少文件系统抽象,因为:

  • 在开发人员的机器上,大量的go run和构建不需要由嵌入(同时可选压缩)资产的开销负担。 文件系统抽象将允许在开发过程中轻松_故障转移_到本地文件系统。

  • 资产可以在开发过程中主动更改,例如,Web 应用程序中的完整 Javascript 前端。 在嵌入和本地文件系统而不是嵌入资产之间无缝切换的能力将允许避免仅仅因为资产改变而编译和重新运行 Go 二进制文件。

编辑:总而言之,如果 embed 包可以公开类似文件系统的接口,比 http.FileSystem 更好的东西,它会解决这些问题。

在嵌入和本地文件系统之间无缝切换的能力

当然,这可以在应用程序级别实现,并且超出了本提案的范围,不是吗?

当然,这可以在应用程序级别实现,并且超出了本提案的范围,不是吗?

抱歉,刚刚意识到,我的措辞方式含糊不清。 我没有在 embed 包中提出文件系统实现,而只是一个接口,比http.FileSystem更好的东西。 这将使应用程序能够实现任何类型的抽象。

编辑:错别字。

@knadh完全同意,当你也只使用go run时它应该可以工作,Packr 处理这个的方式真的很好。 它知道你的文件在哪里,如果它们没有嵌入到应用程序中,那么它会从磁盘加载它们,因为它基本上认为这是“开发模式”。

Packr 的作者还发布了一个新工具 Pkger,它更侧重于 Go Modules。 那里的文件都与 Go Module 根相关。 我真的很喜欢这个想法,但 Pkger 似乎没有实现从磁盘加载本地开发。 两者的结合将是惊人的 IMO。

我不知道它是否已经停止运行,但是虽然“嵌入包方法”非常神奇,但它也提供了一些很棒的功能,因为该工具可以根据调用推断出对文件执行的操作。 例如,API 可能类似于

package embed
func FileReader(name string) io.Reader {…}
func FileReaderAt(name string) io.ReaderAt {…}
func FileBytes(name string) []byte {…}
func FileString(name string) string {…}

如果 go 工具发现对FileReaderAt的调用,它就知道数据必须是未压缩的。 如果它只找到FileReader调用,它就知道它可以存储压缩数据。 如果它找到一个调用FileBytes ,它知道它需要做一个复制,如果它只找到FileString ,它知道它可以从只读内存中提供服务。 等等。

我不相信这是为 go 工具正确实现这一点的合理方法。 但我想提一下这一点,因为它允许在没有任何实际旋钮的情况下获得压缩和零拷贝嵌入的好处。

[编辑] 当然,也让我们在事后添加这些额外的修改内容,首先关注更小的功能集 [/编辑]

如果它只找到 FileReader 调用...

这将排除通过反射使用其他方法。

[编辑] 实际上,我认为其含义比这更广泛。 如果使用FileReaderAt表示数据必须是未压缩的,那么将FileReaderAt()与任何非const输入一起使用意味着所有文件都必须未压缩存储。

我不知道这是好是坏。 我只是认为神奇的启发式方法不会像乍一看那样有用。

一个支持注释 pragma ( //go:embed ) 而不是特殊目录名称 ( static/ ) 的论点:注释允许我们在包的测试存档中嵌入一个文件(或 xtest 存档) ) 但不是正在测试的库。 注释只需要出现在_test.go文件中。

我希望这将解决模块的一个常见问题:如果另一个包在另一个模块中,则很难访问另一个包的测试数据。 一个包可以为其他测试提供数据,在_test.go文件中带有类似//go:embedglob testdata/*的注释。 该包可以导入到常规的非测试二进制文件中,而无需拉入这些文件。

@fd0

嵌入文件如何自动与构建缓存一起工作?

它仍然会起作用。 嵌入的文件内容哈希将混合到缓存键中。

是否有可能(甚至是一个好主意)拥有一个实际上透明的模块/包/机制,就像在您的应用程序内部一样,您只需尝试打开一条路径

internal://static/default.css

并且 File 函数将从二进制文件内部或其他位置读取数据
Package.Mount("internal[/<folder>.]", binary_path + "/resources/")

使用二进制文件中的所有文件创建“内部://”,回退到可执行路径/资源/如果在开发模式下或者在二进制文件中找不到文件(并且可能会抛出警告或用于记录目的的东西)

这将允许例如有

Package.Mount("internal", binary_path  + "/resources/private/")
Package.Mount("anotherkeyword", binary_path  + "/resources/content/")

在“发布”模式下,最好将备用位置锁定到可执行文件的路径,但在开发模式下放宽这一点(只允许 go_path 中的文件夹或类似的东西)

默认情况下,包“挂载” internal:// 或其他一些关键字,但如果他/她想要,让用户重命名它......例如 .ReMount("internal","myCustomName") 或类似的东西。

另一件事......检查备用位置的上次更改/修改时间并自动覆盖内部文件是否有意义,如果应用程序外部有这样的文件(可能有一个标志允许这样做,由程序员在构建之前配置)
这对于应用程序的超快速补丁可能是需要的,在这种情况下,您不想等待创建和分发新版本..您只需创建文件夹并将文件复制到那里,二进制文件就会切换到新的文件。

在 Windows 上,使用资源是否可行或有意义(如资源中的二进制数据 blob)
有点不相关,但也许这个包还可以处理可执行文件中的捆绑图标,清单数据,甚至其他资源? 我意识到它只是Windows...
我想构建器可以记录备用文件夹中文件的上次修改/更改日期,并且仅在文件更改并将 blob 缓存在某处时才触发“创建 blob 数据”。
也许只创建一个“缓存”文件是用户选择对这些捆绑文件启用压缩(如果最终决定压缩它们)......如果选择压缩,则只有被修改的特定文件必须在构建时重新压缩,其他文件只会从缓存复制到二进制文件中。

我看到的一个问题是,如果包允许自定义名称,则需要有某种黑名单,例如不允许“udp、文件、ftp、http、https 和其他各种流行关键字”

至于存储为字节数组/字符串或压缩......恕我直言,无论做出什么决定,它都应该留出将来轻松更新的空间......例如,您可以从不压缩开始,只需要一个偏移量和文件大小的列表,文件名,但可以在将来轻松添加压缩(例如 zlib、lzma、压缩大小、未压缩大小,如果需要分配足够的内存来解压缩块等)。

如果可执行文件可以使用 UPX 或等效文件打包,我个人会很高兴,我假设二进制文件将在内存中解压缩并且一切正常。

一些间接相关的想法:

  • 我喜欢package embed使用 Go 语法的方法
  • 我认为对压缩和其他操作的需要与二进制大小无关,而是关于希望在存储库中存储最差异友好的内容形式,因为没有“不同步”状态有人忘记重新生成并在更改“源”时提交压缩形式,并使包保持“可获取”。 没有解决这些问题,我们只是在解决标准化问题,这可能是可以接受的,但似乎并不理想。
  • 我认为,如果embed交互可以选择提供“编解码器”,我们可以避免需要工具链主动支持特定的压缩/转换。 究竟如何定义编解码器取决于集成语法,但我想像
package embed

type Codec interface {
    // Encode transforms a source representation to an in-binary encoded asset.
    Encode(io.Writer, io.Reader) error

    // Decode transforms an in-binary asset to its active representation that the embedded application wants to use.
    Decode(io.Writer, io.Reader) error
}

这可以涵盖非常具体的用例,比如这个人为的用例:

package main

func NewJSONShrinker() embed.Codec {
   return jsonShrinker{}
}

type jsonShrinker struct{}
func (_ jsonShrinker)  Encode(io.Writer, io.Reader) error {
    // use json.Compact + gzip.Encode...
}
func (_ jsonShrinker)  Decode(io.Writer, io.Reader) error {
    // use gzip.Decode + json.Indent
}

使用它可能看起来像

// go:embed file.name NewJSONShrinker

func main() {
    embed.NewFileReader("file.name") // codec is implied by the comment above
}

或者可能

func main() {
    f, err := embed.NewFileReaderCodec("file.name", NewJSONShrinker())
    ...
}

在第二种形式中,工具链需要静态地理解要使用哪个编解码器,因为它必须在编译时执行Encode步骤。 因此,我们必须禁止在编译时无法轻松确定的任何编解码器值。

鉴于这两个选项,我想我会选择魔术评论加编解码器。 它产生了一个更强大的功能,可以解决这里的所有既定目标。 另外,我不认为魔术评论在这里是不可接受的。 为此,我们现在已经通过go:generate容忍它们。 如果有的话,人们可能会认为单独使用魔法包更像是对当前习语的背离。 Go 生态系统现在没有很多功能可以让源文件指示工具链使用其他源文件,我认为现在唯一不是魔术注释的功能是import关键字。

如果我们进行压缩,则根本没有编解码器类型或压缩级别旋钮。 也就是说,根本没有任何旋钮是根本不支持压缩的最大论据。

如果有的话,我想公开的唯一选择是:随机访问与否。 如果您不需要随机访问,工具和运行时可以选择任何合适的压缩,而不会将其公开给用户。 而且它可能会随着时间的推移而改变/改进。

但是我在@rsc方面没有压缩,因为我意识到:最可压缩的内容(HTML、JS、CSS 等)是您仍然希望随机访问的内容(以例如,通过支持范围请求的http.FileServer提供服务)

看看我们嵌入的 Perkeep 的 HTML/CSS/JS 的总大小,它是 48 KB 未压缩。 Perkeep 服务器二进制文件为 49 MB。 (我忽略了嵌入图像的大小,因为它们已经被压缩了。)所以看起来它只是不值得,但可以稍后添加。

从与@rsc的讨论

在包运行时,

package runtime

type Files struct {
     // unexported field(s), at least 1 byte long so Files has a unique address
}

func (f *Files) Open(...) (...) { ...}
func (f *Files) Stat(...) (...) { ...}
func (f *Files) EnumerateSomehow(...) { ...}

然后在你的代码中:

package yourcode

//go:embed static/*
//go:embed logo.jpg
var website runtime.Files

func F() {
     ... = website.Open("logo.jpg")
}

然后 cmd/go 工具将解析go:embed注释并使用&website对这些模式进行 glob 处理 + 散列这些文件并在运行时注册它们。

运行时将有效地将每个文件地址映射到其内容以及文件可执行文件中的位置(或它们的 ELF/etc 部分名称是什么)。 也许他们是否支持或不支持随机访问,如果我们最终进行任何压缩。

@gdamore

这里还有一个问题——我有一个不同的项目,它使用 DTrace 源代码(嵌入式)。 这对 n 和 rn 之间的差异很敏感。
...
如果各种改造都不起作用,那么这就是反对使用这种新设施的论据。

您还可以在运行时删除任何从运行 go install 的 Windows 用户嵌入的回车。 我已经写了几次 io.Reader 过滤器。

但是我在@rsc方面没有压缩,因为我意识到:最可压缩的内容(HTML、JS、CSS 等)是您仍然希望随机访问的内容(以例如,通过支持范围请求的 http.FileServer 提供服务)

压缩和随机访问并不完全相互排斥。 参见这里的一些讨论: https :

压缩和随机访问并不完全互斥

是的,如果我们想要粗粒度的搜索和一些开销来到达正确的位置。 我在这个领域用CRFS 的 stargz 格式做了一些工作。 但我担心开销会大到我们不想自动为人们做这件事。 我想你也可以懒洋洋地将它膨胀到内存中(并且能够将它放在 GC 上,比如 sync.Pool),但它似乎不值得。

我担心开销会大到我们不想自动为人们做这件事。

很公平。 重要的问题是,如果需要改变或者实验表明开销是可以接受的,我们是否更喜欢一个 API,它允许我们在以后廉价地改变我们的想法。

@bradfitz好点。 我当然可以做到。 FWIW,在我的 repo 中,我还将 git 配置为在查看 .d 文件时毒性较小。 我仍然发现带有反引号的嵌入字符串的属性很有用,因为它是可预测的,不受 git 或系统的奇思妙想的影响。

我对 Codec 的想法是,压缩并不是人们可能想要的唯一转换,而且用户提供的 Codec 类型允许工具链忽略“哪个编解码器”以外的标志。 任何压缩级别或算法,压缩或其他,都必须特定于所使用的编解码器。 我完全同意,在提供一些特定的格式和旋钮的意义上尝试“支持压缩”将是一场疯狂的追逐,拥有人们可能要求的所有变化。 事实上,我会对不常见的用途感到最兴奋,比如预处理 i18n 数据,或者像latlong那样处理数据集,所以我认为仍然值得考虑围绕它的选项。

我确实想到了另一种方法来提供可能更令人愉快的相同灵活性。 // go:embed指令可以是命令调用,就像// go:generate 。 对于最简单的情况,类似

// go:embed "file.name" go run example.com/embedders/cat file.name

当然,关键区别在于命令调用的标准输出嵌入在提供的名称下。 该示例还使用带有go run的假装包来展示如何使命令操作系统独立,因为cat可能无法在 Go 编译的任何地方使用。

这负责转换的“编码”步骤,也许“解码”步骤的任务可以留给用户。 运行时/嵌入包可以只提供用户要求工具链嵌入的字节,无论编码如何。 这很好,因为用户知道解码过程应该是什么。

这样做的一个很大的缺点是,除了嵌入的字节是 zip 或其他东西之外,我没有看到以这种方式嵌入多个文件的好方法。 这实际上可能已经足够了,因为 zip 命令仍然可以使用 glob,并且它在定义方面,您真正关心 glob。 但是我们也可以从这个提案中获得两个特性,一个是简单的嵌入,另一个是运行生成器嵌入。

我想到的一个可能的缺点是它在构建中添加了一个开放式步骤,假设嵌入应该由go build并且不需要像go generate那样的额外工具链调用。 不过我觉得没关系。 也许该工具可以管理自己的缓存以避免重复昂贵的操作,或者它可以与工具链通信以使用 Go 的缓存。 这听起来像是一个可以解决的问题,并且符合go build为我们做更多事情的总体主题(比如获取模块)。

这个项目的目标之一是确保 Go 构建不需要外部工具和 go:generate 行吗?

如果没有,似乎值得保持简单并且只支持字节切片或字符串,因为如果用户想要使用大量旋钮进行压缩,他们可以在他们的 make 文件(或类似文件)中这样做,在构建之前去生成行等,因此,无论该提案的最终结果如何,似乎都不值得将它们添加到其中。

如果不需要 Make 或类似的目标,那么我认为使用压缩可能是有意义的,但就我个人而言,我会尽快使用 Make、go generate 等进行压缩,然后保持简单嵌入并嵌入一些字节.

@SamWhited

这个项目的目标之一是确保 Go 构建不需要外部工具和 go:generate 行吗?

是的。

如果人们想使用 go:generate 或 Makefiles 或其他工具,他们今天有几十种选择。

我们想要一些默认情况下可移植、安全且正确的东西。 (并且要明确:安全意味着我们不能在“go install”时运行任意代码,原因与默认情况下 go:generate 不运行的原因相同)

@斯蒂芬斯2424

我认为,如果嵌入交互可以选择提供“编解码器”,我们可以避免需要工具链主动支持特定的压缩/转换。

go build期间没有任意代码执行。

go build 期间不会执行任意代码。

是的,我现在看到了。 我想没有办法协调只将“源”文件提交到存储库,希望嵌入“处理过的”文件,使包“可获取”,_并且_保持go build简单和安全。 我仍然在这里推动标准化,但我想我也希望有我的蛋糕并吃掉它。 值得一试! 感谢您发现问题!

@flimzy

这将排除通过反射使用其他方法。

我所说的没有方法,只有函数。 它们在运行时是不可发现的,如果不在源代码中按名称提及它们,就无法引用它们。 请注意,不同函数返回的接口值不必是相同的类型——实际上我希望它们要么是具有实现该接口所需的方法的未导出类型,要么是*strings.Reader的实例

不过可以说,这个想法会受到将 embed 包的导出函数作为值传递的影响。 尽管即使那样也不成问题——签名包含一个未导出的类型(见下文),所以你不能声明变量、参数或返回它们的类型。 理论上,您可以将它们传递给reflect.ValueOf自己。 我什至不知道这是否允许您实际调用它们(您仍然必须构造一个未导出的参数类型的值。不知道如果反射允许)。

但尽管如此:如果embed任何顶级函数被用作值并假设它对所有嵌入文件创建的限制,仍然可能(并且最简单)简单地悲观。 这意味着如果您决定使用 embed-package

实际上,我认为其含义比这更广泛。 如果 FileReaderAt 的使用表明数据必须是未压缩的,那么 FileReaderAt() 与任何非常量输入的使用意味着所有文件必须以未压缩的方式存储。

允许非常量输入是没有意义的,因为需要静态地知道文件名才能进行嵌入。 我使用string作为文件名参数的类型是不精确的:它们应该真的是一个未导出的type filename string并且除了函数参数之外不能用作任何东西。 这样,就不可能传递任何不是无类型字符串常量的东西。

@Merovius

允许非常量输入是没有意义的

我想我们在谈论不同的事情。 我的意思是访问器函数的输入(即FileReaderAt() )我相信你会同意非常量输入在那里有意义。

我的观点是:假设我们已经嵌入了 100 个文件,但是我们有一个FileReaderAt(filename)调用,其中filename不是常量; 无法知道哪些(如果确实有的话)嵌入文件将以这种方式访问​​,因此所有文件都必须未压缩存储。

@flimzy我们在谈论同样的事情,我真的认为非常量文件名没有意义:) 想一想,这是错误的和疏忽的。 对于那个很抱歉。 全局或包含整个目录然后遍历它们的工具实际上非常重要,是的。 仍然认为这可以解决 - 例如通过为每个集合(dir/glob)做出决定并只允许通过常量名称选择它们- 但正如我所说:它实际上不是一个 API,我认为它非常适合 Go 工具,因为这是多么神奇。 因此,像这样进入杂草可能会在讨论中为这个概念提供比应有的更多空间:)

我在之前的消息中没有看到的另一种情况,这让我考虑将文件嵌入到 Go 二进制文件中是不可能使用常规 go build/install 正确分发 C 共享库的包装包(共享库保留在来源)。

我最终没有这样做,但这肯定会让我重新考虑这种情况。 C 库确实有很多依赖项,它们作为共享库更容易分发。 这个共享库可以被 Go 绑定嵌入。

哇!!!

@Julio-Guerra
我很确定您仍然需要将它们提取到磁盘,然后使用 dlopen 和 dlsym 来调用 C 函数。

编辑:有点误解了你的帖子,才意识到你在谈论创建一个用于分发的二进制文件

在 http 静态资产之外,对于需要指向内存中的指针的嵌入式 blob,最好有一个函数返回指向已在进程中的嵌入式内存的指针。 否则必须分配新内存并从 io.Reader 复制一份。 这将消耗两倍的内存。

@glycerine ,再次,这是一个stringstring是一个指针和一个长度。

有一些方法来标记要在编译时执行的代码并在运行时提供结果不是很好吗? 这样你就可以读取任何文件,如果你愿意,可以在编译时压缩它,在运行时你可以访问它。 这适用于某些计算,因为它适用于文件内容的预加载。

@burka正如之前在线程中所说, go build不会运行任意代码。

@burka ,这显然超出了范围。 该决定(在编译时不执行代码)是很久以前做出的,这不是更改该策略的错误。

这个提议的一个副作用是 go 代理永远不能将它们存储的文件优化为只有 go 文件。 代理必须存储整个存储库,因为它不知道 Go 代码是否嵌入了任何非 Go 文件。

我不知道代理是否已经为此进行了优化,但在他们可能想要的某一天进行优化是可行的。

@leighmcculloch我不认为今天也是这种情况。 Go 包中的任何非 Go 文件都应包含在模块存档中,因为go test可能需要它们。 作为另一个示例,您可能还有用于 cgo 的 C 文件。

这是一个令人兴奋的方向,我们的用例肯定需要它。

也就是说,我觉得有不同的用例有不同的要求,但大多数评论他们认为应该完成的 _how_ 的人都在暗中设想他们自己的用例,但没有明确定义它们。

如果我们能够描述文件嵌入解决方案的挑战,这可能会有所帮助——至少对我来说真的很有帮助。

例如,我们的主要用例是嵌入 HTML+CSS+JS+JPG+etc,这样当 go 应用程序运行时,它可以将这些文件写入一个目录,这样它们就可以由http.FileServer . 鉴于这个用例,我读过的关于 Readers 和 Writers 的大部分评论对我来说都是陌生的,因为我们不需要从 Go 访问文件,我们只是让go-bindata将它们复制到磁盘 _(尽管也许有一种方法可以利用更好的技术,我们只是还没有意识到我们应该考虑。)_

但我们面临的挑战如下:我们通常使用 GoLand 及其调试器,并将致力于 Web 应用程序的持续更改。 所以在开发过程中,我们需要http.FileServer直接从我们的源目录加载文件。 但是当应用程序运行时, http.FileServer需要从嵌入解决方案写入文件的目录中读取这些文件。 这意味着当我们编译时,我们必须运行 go-bindata 来更新文件,然后将它们检入 Git。 这通常适用于go-bindata ,尽管肯定不是想法。

然而在其他时候我们需要实际运行一个编译的可执行文件,所以我们可以将调试器附加到正在运行的程序,但仍然让该程序从源目录而不是从go-bindata写入嵌入文件的目录加载文件

所以这些是我们的用例和挑战。 也许其他人可以明确定义其他用例和相关挑战集,因此这些讨论可以明确解决各种问题空间和/或明确表示这项工作不会解决给定问题空间的特定需求?

提前感谢您的考虑。

由于我没有将它作为用例提及,因此我们也可以从我们通过 template.ParseFiles 访问的模板目录中受益。

我会发现最干净的方法是通过go.mod选择加入的方法。 这将确保它向后兼容(因为现有项目必须选择使用它)并允许工具(例如 go 代理)确定需要哪些文件。 可以更新go mod init命令以包含新项目的默认版本,以便将来更容易使用。

我可以看到让目录成为标准名称的参数(如果我们需要选择加入,那么它可以是一个更清晰/更简单的名称)或让目录名称在go.mod本身中定义并允许用户选择名称(但默认值由go mod init

在我看来,这样的解决方案在易用性和更少的“魔力”之间取得了平衡。

@jayconrod写道:

一个支持注释 pragma (//go:embed) 而不是特殊目录名 (static/) 的论点:注释允许我们在包(或 xtest 存档)的测试存档中嵌入一个文件,而不是库正在测试中。

这是一个非常好的观察。 虽然如果我们想使用特殊的目录名称,我们可以使用熟悉的机制: static用于所有构建, static_test用于测试构建, static_amd64用于 amd64 构建,以及很快。 不过,我没有看到提供任意构建标记支持的明显方法。

静态目录中可能有一个清单文件(当给定一个空清单时,默认情况下包含除清单之外的所有内容),其中包含 globs 并允许指定构建标签,可能还有以后的压缩等。

一个好处是,如果 go list 命中一个包含清单的目录,它可以跳过那棵树 #30058

一个缺点是它可能会得到非常多的 htacces,不用了,谢谢

将文件捆绑到包中的简单零旋钮机制可以是包目录中的特殊目录go.files (类似于模块中的go.mod )。 访问将仅限于该包,除非它选择导出一个符号。

编辑:单功能runtime/files提案:

package files

func Open(name string) (io.ReadCloser, error) {
    // runtime opens embedded file based on caller package
    return rc, nil
}
package foo

import "runtime/files"

func ReadPackageFile(name string) ([]byte, error) {
    rc, err := files.Open(name)
    if err != nil {
        return nil, err
    }
    defer rc.Close()
    return ioutil.ReadAll(rc)
}

import "C"方法已经为“神奇”导入路径开创了先例。 海事组织它已经很好地解决了。

由于我没有将它作为用例提及,因此我们也可以从我们通过 template.ParseFiles 访问的模板目录中受益。

还有另一个挑战:虽然二进制文件可能包含所有需要的文件,但这些文件将是我作为开发人员提供的默认值。 然而,诸如印记或隐私政策之类的模板必须可由最终用户自定义。 据我所知,这意味着必须有某种方法可以导出我的默认文件,然后让二进制文件在运行时使用自定义文件,或者某种方法用自定义文件替换嵌入版本。

我认为这可以通过提供具有“导出”和“替换”嵌入式资源功能的 API 来完成。 然后,开发人员可以向最终用户提供一些命令行选项(在内部使用提到的 API 调用)。

当然,所有这些都是基于这样一个假设,即实际上会有某种嵌入,这肯定会简化部署。

感谢您打开问题。 在工作中,我们考虑了相同的功能想法,因为我们需要在几乎每个 Golang 项目中嵌入文件。 现有的库工作正常,但我认为这是 Golang 所追求的功能。 它是一种语言,可以变成单个静态二进制文件。 它应该通过允许我们将所需的资产文件加载到二进制文件中来接受这一点,并使用开发人员友好和通用的 API。

我只想快速提供我喜欢的实现细节。 很多人谈到自动提供一个 API 来读取嵌入的文件,而不是需要另一个信号,如魔术注释。 我认为这应该是要走的路,因为它为该方法提供了熟悉的编程语法。 使用一个特殊的包,可能是前面提到的runtime/embed ,可以满足这一点,并且可以在未来轻松扩展。 像下面这样的实现对我来说最有意义:

type EmbedPackage interface {
    Bytes(filename string) []bytes
    BytesCompressed(filename string, config interface{}) []bytes // compressed in-binary as configured by some kind of config struct, memoizes decompression during runtime on first access
    Reader(filename string) io.Reader
    File(filename string) os.File // readonly and contains all metadata
    Dir(filepath string) []os.File 
    Glob(pattern string) []os.File // like filepath.Glob()

    // maybe? this could allow to load JSON, YAML, INI, TOML, etc files more easily
    // but would probably be too much for the std lib implementation
    Unmarshal(filename string, config interface{}, ptr interface{}) 
}

在代码中的某处使用该包应该会触发编译器通过自动嵌入来将该文件提供给运行时。

// embed a file that is compressed in-binary and automatically decompressed on first access
var LongText = embed.BytesCompressed("legal.html", embed.Config{ Compression: "gzip", CompressionLevel: "9" })

// loads a single file as reader for easy access
var FewLinesOfText = bufio.NewReader(embed.Reader("lines.txt"))
for _, line := range FewLinesOfText.ReadLines() { ... }

// embeds all files in the directory
var PdfFontFiles = embed.Dir("/fonts")

// unmarshals file into custom config
var PdfProcessingConfig MyPdfProcessingConfig
embed.Unmarshal("/pdf_conversion.json", embed.Config{ Encoding: "text/json" }, &PdfProcessingConfig)

此外,我认为如果我们将导入限制在 go.mod 目录下或可能 1 个目录级别下的文件,那么安全问题和可重复性应该不是问题,这在之前的线程中也已经提到过。 绝对嵌入路径将相对于该目录级别进行解析。

在编译过程中未能访问文件将产生编译器错误。

还可以在二进制文件后面创建 zip 存档,以便它可以有效地成为自解压二进制文件。 也许这在某些用例中很有用? 在这里做了一个实验: https :

Go 已经有了“testdata”。 单元测试使用常规 IO 来做他们想做的任何事情。 测试范围意味着内容不发货。 这就是所有需要知道的,没有多余的装饰,没有魔法,没有压缩的容器逻辑,没有可配置的间接,没有 META-INF。 美丽,简单,优雅。 为什么没有捆绑运行时范围依赖项的“数据”文件夹?

我们可以轻松地扫描 Github ea 中现有的 Go 项目,并提出一些已经使用“data”文件夹的项目,因此需要进行调整。

另一件事我不清楚。 对于static目录的讨论,我不是 100% 清楚我们是在讨论static _source_目录,还是static目录,其中文件将可用_at运行时_?

这种区别特别重要,因为它涉及开发过程和正在开发的调试代码。

@mikeschinkel从原始帖子中可以清楚地看出嵌入将在构建时发生:

使go install / go build自动嵌入

原始帖子和上面的一些评论还讨论了在运行时加载文件的“开发”模式。

@mvdan感谢您的回答。 所以您认为这意味着建议的/static/目录将相对于应用程序存储库、包存储库和/或两者的根目录?

运行时文件的位置将完全取决于开发人员想要放置它们的位置?

如果这一切都是真的——而且看起来确实合乎逻辑——那么使用调试信息编译的程序可以有选择地从它们的源位置加载文件以促进调试,而无需大量额外的——非标准化的——逻辑和代码,这将是有帮助的。

上面提到的几个人模块代理。 我认为这是对这个功能的良好设计的一个很好的试金石。

今天似乎有可能在不执行用户代码的情况下实现一个可行的模块代理,它可以去除构建中未使用的文件。 上面的一些设计意味着模块代理必须执行用户代码来确定还包含哪些静态文件。

人们还提到 go.mod 作为选择加入。

想法:go.mod 文件中的规范? 使其他工具可以直接解析。

module github.com/foo/bar

data internal/static ./static/*.tmpl.html

这将在编译时使用文件数据创建一个包。 Glob 语法在这里可能很好,但可能简化并且仅嵌入目录就足够了。 (旁白:+1 表示** glob 语法。)

import "github.com/foo/bar/internal/static"

f, err := static.Open("static/templates/foo.tmpl")

像 StripPrefix 这样的东西在这里可能很好,但不是必需的。 易于创建使用任何您想要的文件路径的包装器包。

可以进一步简化:

module github.com/foo/bar

data ./static/*.tmpl.html
import "runtime/moddata"

moddata.Open("static/foo.tmpl")

但是,根据调用包/模块的不同,moddata 会有不同的行为,这有点不直观。 这会使编写助手变得更加困难(例如,http.Filesystem 转换器)

今天似乎有可能在不执行用户代码的情况下实现一个可行的模块代理,它可以去除构建中未使用的文件。 上面的一些设计意味着模块代理必须执行用户代码来确定还包含哪些静态文件。

我认为这里不会有重大变化。 特别是,C 代码已经可以包含树中的任何文件,因此想要执行此操作的模块代理需要解析 C。似乎到那时,我们引入的任何魔术注释或 API 都将是一小步。

上面的一些设计意味着模块代理必须执行用户代码来确定还包含哪些静态文件。

我认为很明显“go 工具在构建期间不得执行用户代码”是在沙子中绘制的一条线,不会在这里交叉。 如果 go 工具无法执行用户代码,那么必须可以在没有它的情况下告诉要包含哪些文件。

我一直试图将我对这个用例的各种想法浓缩成一些有说服力的东西,因此对@broady 的建议大加+1。 我认为这在很大程度上概括了我一直在思考的问题。 但是,我认为关键字应该是动词embed而不是名词data

  1. 嵌入式文件感觉像是应该导入的东西,而不是只有一个特殊的注释或一个神奇的包。 在 Go 项目中, go.mod文件是开发人员可以指定所需模块/文件的地方,因此扩展它以支持嵌入是有意义的。

  2. 此外,在我看来,如果可以包含一个包而不是使用一次性语法将一些临时添加到 Go 项目中的包,那么嵌入文件的集合会更有价值和可重用。 这里的想法是,如果嵌入作为包实现,那么人们可以通过 Github 开发和共享它们,其他人可以在他们的项目中使用它们。 想象一下 GitHub 上由社区维护和免费使用的包,其中包含:

    一种。 国家/地区的文件,其中每个文件都包含该国家/地区的所有邮政编码,
    湾一个包含所有已知用户代理字符串的文件,用于识别浏览器,
    C。 世界上每个国家的国旗图像,
    d. 描述 Go 程序中常见错误的深入帮助信息,
    e. 等等...

  3. 一种新的 URL 方案,例如goembed:// —— 或者可能是现有的—— 可用于从包中打开和读取文件,从而允许 _(all?)_ 现有文件操作 API 被利用,而不是创建新的的,类似于以下内容,这些内容与当前包中包含的嵌入相关:

    data, err := ioutil.ReadFile("goembed://postal-codes.txt")    
    if (err != nil) {
      fmt.Println(err)
    }
    

有了上面的概念,没有什么感觉像_“魔法”_; 一切都将通过一种感觉像是有目的的机制来优雅地处理。 几乎不需要扩展; go.mod一个新动词和一个 Go 内部可以识别的新 URL 方案。 其他一切都将按原样从 Go 中提供。

我现在应该做什么

我现在使用code.soquee.net/pkgzip (它是statik的一个分支,它改变了 API 以避免全局状态和导入副作用)。 我的正常工作流程(至少在 Web 应用程序中)是将资产捆绑在 ZIP 文件中,然后使用golang.org/x/tools/godoc/vfs/zipfsgolang.org/x/tools/godoc/vfs/httpfs为它们提供服务。

去:嵌入方法

有两件事可以证明会阻止我采用go:embed方法:

  1. 生成的代码不会出现在文档中
  2. 资产可能分散在整个代码库中(使用外部工具和go:generate也是如此,这就是为什么我通常更喜欢在构建之前使用 makefile 生成各种资产集,然后我可以在makefile中看到它们)

还有一个我没有在上面提到的问题,因为它可能是一些特性,将资产作为包的一部分(而不是整个模块)意味着构建标签、测试包等的所有复杂性. 适用于它们,我们需要一种方法来指定它们对该包是公共的还是私有的,等等。这似乎有很多额外的构建复杂性。

我喜欢它的一点是可以编写库来使导入资产更容易。 例如。 一个带有单个 Go 文件的库,它只嵌入了一种字体,或者可以发布一些图标,我可以像任何其他 go 包一样导入它。 将来,我只需导入即可获得图标字体:

import "forkaweso.me/forkawesome/v2"

嵌入包方法

虽然我喜欢让所有这些都是明确的、正常的 Go 代码的想法,但我讨厌这将是另一个无法在标准库之外实现的魔法包的想法。

这样的包会被定义为语言规范的一部分吗? 如果不是,这是 Go 代码在不同实现之间会中断的另一个地方,这也感觉很糟糕。 我可能会继续使用外部工具来防止这种损坏。

此外,正如其他人所提到的,这是在构建时完成的这一事实意味着该包只能将字符串文字或常量作为参数。 目前没有办法在类型系统中表示这一点,我怀疑这会引起混淆。 这可以通过引入诸如常量函数之类的东西来解决,但现在我们正在谈论主要的语言变化,使其成为一个非入门者。 否则我看不到解决这个问题的好方法。

杂交种

我喜欢混合方法的想法。 与其重用评论(最终散落在各处,而且就个人而言,感觉很恶心),我希望将所有资产放在一个地方,可能是go.mod文件和其他文件讲过了:

module forkaweso.me/forkawesome/v2

go 1.15

embed (
    fonts/forkawesome-webfont.ttf
    fonts/forkawesome-webfont.woff2
)

这意味着在不创建单独模块的情况下,资产不能被任意构建标签或任意包(例如 _testing 包)包含或排除。 我认为这种降低复杂性可能是可取的(您尝试导入的库中没有隐藏构建标记,并且您无法弄清楚为什么没有正确的资产,因为导入库应该已经嵌入了它) ,但 YMMV。 如果这是可取的,仍然可以使用类似 pragma 的注释,只是它们不生成代码,而是使用与我即将为go.mod版本描述的方法相同的方法。

与原始提案不同,这不会生成任何代码。 相反,例如功能。 读取 ELF 文件的数据部分(或者最终存储在您使用的任何操作系统上)将在适当的地方添加(例如osdebug/elf等)和然后,可选地,将创建一个新的包,它的行为与 OP 中描述的包完全一样,只是它不是魔术并自己进行嵌入,而是仅读取嵌入的文件(这意味着它可以在标准库之外实现)如果需要)。

这可以解决诸如必须将魔法包限制为仅允许字符串文字作为参数之类的问题,但这确实意味着更难检查嵌入的资产是否在任何地方实际使用或最终成为无意义的资产。 它还避免了标准库包之间的任何新依赖关系,因为唯一需要导入任何额外内容的包是新包本身。

var IconFont = embed.Dir("forkaweso.me/forkawesome/v2/fonts/")
var Logo = embed.File("images/logo.jpg")

如上所示,如果需要,将资源放在模块中仍然可以将它们的范围限定到该特定模块。 实际的 API 以及您如何选择资产可能需要一些工作。

还有一个想法:与其在go.mod中添加一种新的动词embed ,我们可以引入一种新的包,一个数据包,它被导入并在 go.mod 中使用通常的方式。 这是一个稻草人素描。

如果一个包正好包含一个.go文件static.go ,并且该文件只包含注释和一个包子句,那么一个包就是一个数据包。 导入时,cmd/go 使用导出的函数填充包,提供对其中包含的文件的访问,这些文件嵌入在生成的二进制文件中。

如果它是一个实际的包,那将意味着internal规则将适用,我们可以在不添加到 API 的情况下进行访问控制。

那么在目录中自动包含所有非.go文件和子文件夹(遵循无实际代码规则)怎么样?

如果一个包正好包含一个.go文件static.go ,并且该文件只包含注释和一个包子句,那么一个包就是一个数据包。

这个检查会在应用构建标签之前完成吗? 如果是这样,这似乎是另一种特殊情况,可能需要避免。 如果不是,那么很可能一个包可能被视为某些构建标签的标准 Go 包,而其他人则被视为数据包。 这看起来很奇怪,但也许它是可取的?

@flimzy
这将允许人们使用带有一个标签的嵌入文件,并定义与生成的包相同的 fns/vars,并以另一种方式(可能是远程?)使用另一个标签提供文件。

如果有一个构建标志来生成包装器函数,那么我们只需要填写空白就可以了。

@josharian

如果一个包正好包含一个.go文件static.go ,并且该文件只包含注释和一个包子句,那么一个包就是一个数据包。

我可以设想“数据”包具有自己的特定于域的功能,例如邮政编码查找。 您刚刚提出的方法将不允许原始数据以外的任何内容,从而使能够将逻辑与数据打包在一起的好处无效。

我可以设想“数据”包具有自己的特定于域的功能,例如邮政编码查找。

您可以在 my.pkg/postalcode 中公开功能并将数据放入 my.pkg/postalcode/data(或 my.pkg/postalcode/internal/data)。

我看到按照您建议的方式进行操作的吸引力,但它引发了一系列问题:向后兼容性如何工作? 您如何标记数据包? 如果包的功能与 cmd/go 将添加的那些功能冲突,你会怎么做? (我并不是说这些没有答案,只是不必回答它们更简单。)

@josharian ,请考虑上面的类型检查评论(https://github.com/golang/go/issues/35950#issuecomment-561443566)。

@bradfitz是的,这将是语言更改,并且需要 go/types 支持。

实际上,有一种方法可以在不改变语言的情况下做到这一点——要求 static.go 包含与 cmd/go 将填充的内容完全匹配的无正文函数。

要求 static.go 包含与 cmd/go 将填充的内容完全匹配的无正文函数。

如果它生成每个文件函数而不是捕获所有embed.File() ,则可以轻松实现每个资产的导出控制。

所以就像生成的东西看起来像:

EmbededFoo() embed.Asset {...}
embededBar() embed.Asset {...}

我在 4 个月前写的一篇关于静态文件的博客文章。 见结论中的最后一句话:-)

@josharian

您可以在 my.pkg/postalcode 中公开功能并将数据放入 my.pkg/postalcode/data(或 my.pkg/postalcode/internal/data)。

那 - 虽然不雅 - 可以解决我的担忧。

向后兼容性如何工作?

我不明白 BC 问题如何适用于此。 你能详细说明一下吗?

您如何标记数据包?

使用go.modembed语句?

也许我没有按照你的要求去做。

但我会扭转局面; 你如何标记一个只有数据的包?

如果包的功能与 cmd/go 将添加的那些功能冲突,你会怎么做?

  1. 使用建议的方法,我认为这些不需要 cmd/go 添加任何功能。

  2. 即使 cmd/go 确实需要添加函数,我认为 _existing_ 包中的冲突行为将是 _undefined_。

    该提案假设开发人员遵循单一职责原则,因此只应将包含数据的包构建为以数据为中心的包,而不应将数据添加到现有的以逻辑为中心的包中。

    当然,开发人员_可以_添加到现有包中,在这种情况下,行为将是未定义的。 IOW,如果开发人员忽略习语,他们将处于未知领域。

我并不是说这些没有答案,只是不必回答它们更简单。

除了我认为答案很简单。 至少对于你迄今为止摆过的那些。

我认为任何为符号添加符号或值的解决方案都必须是包范围的,而不是模块范围的。 因为 Go 的编译单元是包,而不是模块。

因此,这省略了任何使用go.mod来指定要导入的文件列表的情况。

@dolmen如果最终结果在它自己的包中,那么范围本身就是一个模块。

@urandom不,范围是导入生成包的包。 但无论如何,我认为完整生成的包不在本提案的范围内。

@urandom不,范围是导入生成包的包。 但无论如何,我认为完整生成的包不在本提案的范围内。

无论该提案如何实施,鉴于各种模块包将使用最终结果,因此在模块级别指定嵌入内容的定义是有道理的。 在 java 生态系统中,这方面的先例也已经存在,其中嵌入的文件是模块范围的,并从魔法目录中添加。

此外,go.mod 提供了最简洁的方式来添加这样的功能(没有魔法注释或魔法目录),而不会破坏现有程序。

这里有一些尚未提及的内容:Go 源代码处理工具(编译器、静态分析器)的 API 与运行时 API 一样重要。 这种 API 是 Go 的核心价值,有助于发展生态系统(如go/ast / go/formatgo mod edit )。

这个 API 可以被预处理器工具使用(特别是在go:generate步骤中)来获取将被嵌入的文件列表,或者它可以用来生成引用。

在特殊包的情况下,我认为go.mod解析( go mod工具)或go/ast解析器没有任何变化。

@dolmen

_"我认为任何为符号添加符号或值的解决方案都必须是包范围的,而不是模块范围的。因为 Go 的编译单元是包,而不是模块。所以这省略了任何使用 go.mod 来指定的列表要导入的文件。”_

什么是模块? 模块是 _“相关的 Go 包的集合,它们作为一个单元一起进行版本控制。”_因此一个模块可以由单个包组成,而单个包可以是一个模块的整体。

因此,假设 Go 团队采用go.mod不是特殊注释和魔法包,则go.mod是指定要导入的文件列表的正确位置。 除非 Go 团队决定添加go.pkg文件。

此外,如果 Go 团队接受go.mod作为指定嵌入文件的地方,那么任何想要嵌入文件的人都应该提供go.modembed指令,以及包由go.mod文件所在的目录表示的将是包含嵌入文件的包。

但是如果这不是开发人员想要的,他们应该创建另一个go.mod文件并将其放在他们想要包含嵌入文件的包的目录中。

您是否设想过这些约束不可行的合理场景?

@mikeschinkel ,一个模块是_related_包的集合。 然而,使用一个模块中的一个包而不引入该模块中其他包的传递依赖项(和数据!)是可能的(并且合理的!)。

数据文件通常是每个包的依赖项,而不是每个模块的,因此关于如何定位这些依赖项的信息应该与包共存——而不是作为单独的模块级元数据存储。

@bcmills

似乎可以用“模块”替换消息中的“数据文件”,它仍然适用。
将特定模块作为您自己的特定包的依赖项是很常见的。
然而我们把它们都放在了 go.mod 中。

@urandom ,并非go.mod文件中指示的模块中的所有包都链接到最终二进制文件中。 (将依赖项放入go.mod文件_不_等同于将该依赖项链接到程序中。)

元点

很明显,这是很多人关心的事情,布拉德在顶部的原始评论与其说是完成的提案,不如说是最初的草图/起点/行动呼吁。 我认为在这一点上结束这个特定的讨论是有意义的,让 Brad 和其他几个人合作制定详细的设计文档,然后开始一个新的问题来讨论该特定的(尚未编写的)文档. 似乎这将有助于集中精力进行一些庞大的对话。

想法?

我不确定我是否同意关闭这个,但我们可以建议保留它,直到有设计文档并锁定评论。 (此时几乎所有评论都是多余的,因为有太多评论供人们阅读以查看他们的评论是否多余......)

也许当有设计文档时,可以关闭这个文档。

或者关闭这个并重新打开#3035(冻结评论),以便至少有一个未解决的问题对其进行跟踪。

很抱歉在谈论结束后立即这样做,但在@bcmills评论之后和我还没有澄清之前,密切的讨论就开始了。

“_然而,使用一个模块中的一个包是可能的(而且是合理的!),而无需引入该模块中其他包的传递依赖项(和数据!)。”_

是的,显然是 _possible._ 但就像任何最佳实践一样,数据包的最佳实践可能是为模块创建单个包,这可以解决您的问题。 如果这意味着在根目录中有一个go.mod并且在包含数据包的子目录中有另一个go.mod ,那就这样吧。

我想我是在提倡你不要让完美成为这里的好敌人,这里的好被标识为go.mod是指定嵌入文件的理想场所,因为它们的性质是一个列表模块的组成部分。

抱歉,包是 Go 中的基本概念,而不是模块。
模块只是作为一个单元进行版本控制的一组包。
除了单个包的语义之外,模块不会贡献额外的语义。
这是他们简单性的一部分。
我们在这里所做的任何事情都应该与包相关联,而不是模块。

无论这去哪里,不管它完成了,都应该有一种方法来获取将使用 go list 嵌入的所有资产的列表(不仅仅是使用的模式)。

暂时搁置,直到 Brad 和其他人制定正式的设计文档。

阻止某些文件应该不会太难,尤其是当您使用staticembed目录时。 符号链接可能会使这有点复杂,但您可以阻止它嵌入当前模块之外的任何内容,或者,如果您在 GOPATH 上,则在包含目录的包之外。

我不是特别喜欢编译为代码的评论,但我也发现影响编译的伪包也有点奇怪。 如果不使用目录方法,也许在语言中实际内置某种embed顶级声明可能更有意义。 它的工作方式与import类似,但仅支持本地路径,并且需要为其分配名称。 例如,

embed ui "./ui/build"

func main() {
  file, err := ui.Open("version.txt")
  if err != nil {
    panic(err)
  }
  version, err = ioutil.ReadAll(file)
  if err != nil {
    panic(err)
  }
  file.Close()

  log.Printf("UI Version: %s\n", bytes.TrimSpace(version))
  http.ListenAndServe(":8080", http.EmbeddedDir(ui))
}

编辑:你打败了我,@jayconrod。

这是干净和可读的但是我不确定围棋团队是否想要引入一个新的关键字

我记得的想法是只有一个特殊的目录名称,如包含静态数据的“静态”,并自动通过 API 使它们可用,无需注释。

使用static作为特殊目录名称有点混乱,我宁愿使用assets
我在线程中没有看到的另一个想法是允许将assets作为包导入,例如import "example.com/internal/assets" 。 暴露的 API 仍然需要设计,但至少它看起来比特殊注释或新的runtime/files风格的包更干净。

我在线程中没有看到的另一个想法是允许将资产作为包导入

这是在这里提出的: https :

一个复杂的问题是,要启用类型检查,您需要将其作为语言更改或提供由 cmd/go 填写的无正文功能

这是类似的想法,但static.go设计允许将任意导入路径转换为数据包,而assets目录更像testdatainternalvendor就“特别”而言。 assets可能的要求之一是不包含 Go 包(或只允许文档),即隐式向后兼容性。

这也可以与runtime/files -thingy API 结合使用以获取文件。 也就是说,使用原始的import将目录树嵌入文件,然后使用一些运行时包来访问它们。 甚至可能是os.Open ,但这不太可能被接受。

shurcooL/vfsgen一起shurcooL/httpgzip有一个很好的功能,可以在不解压缩的情况下提供内容。

例如

    rsp.Header().Set("Content-Type", "image/png")
    httpgzip.ServeContent(rsp, req, "", time.Time{}, file)

正在为 C++ 提出类似的功能: std::embed

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1040r0.html
https://mobile.twitter.com/Cor3ntin/status/1208389050698215427

作为设计灵感和收集可能的用例,它可能很有用。

我参加聚会有点晚了,但我有个主意。 而不是特殊注释,固定的特殊目录(静态),或扩展 go.mod 的明显禁止方法:每个包的新清单文件怎么样:go.res

  • 包含文件列表。 没有路径或 glob,只有当前包目录中的名称。 如果需要,在提交之前从 glob 生成它。

    • __Edit__ 顶部可能需要一个package mypackagename行,就像 go 文件一样。 或者,您可以在文件名中包含包名称(例如 mypackagename.go.res)。 就个人而言,我更喜欢package标题行。

  • 一个名为“resource”或“io/resource”的新核心包。 至少有一个功能: func Read(name string) (io.Reader, bool)读取当前包中嵌入的资源。

    • __Edit__ 不确定核心包是否以这种方式工作。 可能必须是生成的包私有函数(例如func readresource(name string) (io.Reader, bool)

  • 如果你想要子目录中的资源,那么通过添加一个 go.res 文件和至少一个.go文件来使子目录成为一个包。 go文件导出你自己的公共API,用于访问子目录包中的资源。 go 文件和导出的 API 是必需的,因为来自其他包的资源不会自动导出(按设计)。 您还可以自定义以这种方式导出它们的方式。

    • __Edit__ 或者,如果您需要目录结构和/或压缩,请使用 tar 资源。 这允许诸如 webpack bundles 之类的东西,它们已经需要编译(并且可能受益于预压缩)。 使它们更进一步到达焦油是很简单的。

  • __Edit__ 需要清单吗? 只需包含 go.res 文件本身作为资源。 我们甚至不需要创建一个 listresources 函数。

非常简单。 一项新功能。 一个新文件。 没有路径。 没有压缩。 没有新语法。 没有魔法。 可扩展。 通过阅读器进行只读访问(但将来对其他访问模式开放)。 破坏现有软件包的可能性几乎为零。 保持包是 go 中的核心构造。

__Edit__ 在 github 搜索language:go filename:go.res extension:res ,似乎go.res将是一个非常安全的文件名。 go repos 中没有匹配项,non-go repos 中只有少数匹配项。

我喜欢@chris.ackermanm 的想法。 但我更喜欢组合:

指定目录中命名空间的 go.res 文件。

这允许

  • 多个包含,只要命名空间不同
  • 之前不知道文件,不得不生成一个列表

后一个应该解决 webpack 的输出等问题,这些输出可能会因更新、不同的选项以及您能想到的任何内容而改变布局。

关于压缩:我认为这更像是一个特性,不会使二进制大小爆炸,并且应该对使用代码透明。

稍后您可以允许重写,例如

filename => stored-as.png

只是我的 2 美分

@sascha-andres 似乎超简单和零魔法是这个线程的基调。 请参阅我对您的建议所做的评论的编辑。

我不喜欢映射。 不需要。 无论如何,这可以通过从单独的包中公开您自己的读取函数来实现,现在我们需要一个新的文件语法,或者比 file-per-line 更复杂的东西。

你好

这个提议太棒了!

我有我的方法来嵌入资产。 无需引入 GNU bintools 以外的任何工具。 它有点脏,但现在对我来说效果很好。 我只是想分享它,看看它是否有帮助。

我的方法是将我的资产(使用 tar&gz 压缩)嵌入 elf/pe32 部分,并使用 objcopy,并在需要时通过包 debug/elf 和 debug/pe32 以及 zip 读取它。 我需要记住的只是不要触及任何现有的部分。 所有资产都是不可变的,然后代码读取内容并在内存中处理它。

我对语言设计或编译器设计非常缺乏经验。 所以我会使用上面描述的方法并使用.goassets或类似的东西作为部分名称。 并使压缩成为可选。

我的方法是将我的资产(使用 tar&gz 压缩)嵌入 elf/pe32 部分,并使用 objcopy,并在需要时通过包 debug/elf 和 debug/pe32 以及 zip 读取它。 我需要记住的只是不要触及任何现有的部分。 所有资产都是不可变的,然后代码读取内容并在内存中处理它。

听起来它适用于elf / pe32但是mach-o / plan9呢?

另一个问题是它依赖于打开可执行文件的文件句柄,如果可执行文件已被覆盖/更新/删除,那么这将返回不同的数据,不确定这是合法问题还是意外功能。

我自己做了一些尝试(使用debug/macho ),但我看不到让这个跨平台工作的方法,我在 macOS 上构建,安装的 GNU binutils 似乎破坏了mach-o-x86-64文件(这可能只是我缺乏对mach-o结构的理解,而且我什至看过objcopy太久

另一个问题是它依赖于在可执行文件上打开文件句柄

我很确定程序加载器将(或可能)将资源部分加载到内存中,因此无需使用调试包。 尽管访问数据需要对目标文件进行更多的修改而不是值得。

为什么不遵循有效的方法——例如Java是如何做到的。 我会要求事情是一个大的go-ish,但在线条中的东西:

  • 创建一个go.res文件或者修改go.mod指向资源所在的目录
  • 此目录中的所有文件都会自动包含在内,编译器在最终可执行文件中没有例外
  • 语言提供了一个类似路径的 API 来访问这些资源

压缩等应该在此资源捆绑的范围之外,并且在需要时由任何// go:generate脚本决定。

有人看过markbates/pcker吗? 这是使用go.mod作为当前工作目录的一个非常简单的解决方案。 假设要嵌入index.html ,打开它会是pkger.Open("/index.html") 。 我认为这比在项目中硬编码static/目录更好。

还值得一提的是,据我所知,Go 对项目没有任何重要的结构要求。 go.mod只是一个文件,并不是很多人都使用过vendor/ 。 我个人认为static/目录没有任何好处。

由于我们已经有一种方法可以通过现有的ldflags链接标志-X importpath.name=value将数据注入(尽管有限)到构建中,那么是否可以调整该代码路径以接受-X importpath.name=@filename注入外部任意数据?

我意识到这并没有涵盖原始问题的所有既定目标,但作为现有-X功能的扩展,这似乎是一个合理的进步吗?

(如果这可行,那么将go.mod语法扩展为指定ldflags -X值的更简洁的方式是下一个合理的步骤?)

这是一个非常有趣的想法,但我担心安全隐患。

执行-X 'pkg.BuildVersion=$(git rev-parse HEAD)'很常见,但我们不想让 go.mod 运行任意命令,对吗? (我猜 go generate 可以,但这不是您通常为下载的 OSS 包运行的东西。)如果 go.mod 无法处理,它最终会丢失一个主要用例,因此 ldflags 仍然很常见。

然后是确保@filename不是 /etc/passwd 或其他任何符号链接的另一个问题。

使用链接器排除了对 WASM 的支持,也可能排除了不使用链接器的其他目标。

基于此处的讨论, @bradfitz和我制定了一个设计,该设计位于上述两种方法的中间位置,采用似乎最好的方法。 我已经发布了设计文档草案、视频和代码(链接如下)。 不要对此问题发表评论,请使用 Reddit Q&A 对此特定草案设计发表评论 - Reddit 线程和规模讨论比 GitHub 更好。 谢谢!

视频: https :
设计: https :
问答: https :
代码: https :

@rsc在我看来,go:embed 提案不如在编译时提供 _universal_ 沙盒化 Go 代码执行,后者包括读取文件并将读取的数据转换为最适合运行时使用的 _optimal 格式。

@atomsymbol这听起来超出了这个问题的范围。

@atomsymbol这听起来超出了这个问题的范围。

我知道这一点。

我通读了提案并扫描了代码,但找不到答案:此嵌入方案是否包含有关磁盘上文件 (~os.Stat) 的信息? 或者这些时间戳会被重置以构建时间? 无论哪种方式,这些都是在不同地方使用的有用信息,例如,我们可以基于此为未更改的资产发送 304。

谢谢!

编辑:在 reddit 线程中找到它。

所有嵌入文件的修改时间都是零时间,这正是您列出的可重复性问题。 (出于同样的原因,模块甚至不记录修改时间。)

https://old.reddit.com/r/golang/comments/hv96ny/qa_goembed_draft_design/fytj7my/

无论哪种方式,这些都是在不同地方使用的有用信息,例如,我们可以基于此为未更改的资产发送 304。

基于文件数据散列的 ETag 标头将解决该问题,而无需了解有关日期的任何信息。 但这必须由 http.HandlerFS 或其他东西知道才能工作并且不浪费资源,每个文件只需要完成一次。

但这必须由 http.HandlerFS 或其他东西知道才能工作并且不浪费资源,每个文件只需要完成一次。

http.HandlerFS 如何知道 fs.FS 是不可变的? 是否应该有一个IsImmutable() bool可选接口?

http.HandlerFS 如何知道 fs.FS 是不可变的? 是否应该有一个IsImmutable() bool可选接口?

我不想深入了解实现细节,因为我不是这些东西的设计者,但 http.HandlerFS 可以检查它是否是 embed.FS 类型并将其作为特例进行处理,我认为没有人想要立即扩展 FS API。 HandlerFS 也可能有一个选项参数,专门告诉它将文件系统视为不可变的。 此外,如果这是在应用程序启动时完成的,并且所有 ctime/mtime 的值都为零,handlerFS 可以使用该信息“知道”文件没有更改,但也有文件系统可能没有 mtime 或禁用了它,所以有那里也可能有问题。

我没有看这个问题的评论。

@atomsymbol欢迎回来! 很高兴再次看到你在这里发表评论。
我原则上同意,如果我们有沙箱,很多事情会更容易。
另一方面,许多事情可能更难——构建可能永远不会完成。
无论如何,我们今天绝对没有那种沙箱。 :-)

@kokes我不确定细节,
但我们将确保通过 HTTP 提供 embed.Files 获取 ETags 默认情况下是正确的。

我已提交 #41191 以接受 7 月份发布的设计草案。
我将关闭这个问题,并被那个问题取代。
感谢您在这里进行的精彩初步讨论。

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