如何将文件嵌入到 Go 二进制文件中

Posted

技术标签:

【中文标题】如何将文件嵌入到 Go 二进制文件中【英文标题】:How to embed files into Go binaries 【发布时间】:2013-07-22 19:42:06 【问题描述】:

我有一些从 Go 程序中读取的文本文件。我想发布一个可执行文件,而不额外提供该文本文件。 如何将其嵌入到 Windows 和 Linux 上的编译中?

【问题讨论】:

【参考方案1】:

===== 2021 年 1 月编辑 =====

从 2021 年 2 月发布的 Go 1.16 开始,您可以使用 go:embed 指令:

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))

===== 原始答案======

从 Go 1.4 开始,如果您需要更大的灵活性,可以使用 go generate。

如果您有多个文本文件或文本文件可能会更改,您可能不想硬编码文本文件,而是在编译时包含它。

如果您有以下文件:

main.go
scripts/includetxt.go
a.txt
b.txt

并且想要访问 main.go 中所有 .txt 文件的内容,可以包含一个包含 go generate 命令的特殊注释。

main.go

package main

import "fmt"

//go:generate go run scripts/includetxt.go

func main() 
    fmt.Println(a)
    fmt.Println(b)

go generate 命令将在go:generate 之后运行脚本。在这种情况下,它运行一个 go 脚本,该脚本读取所有文本文件并将它们作为字符串文字输出到一个新文件中。我跳过了较短代码的错误处理。

脚本/includetxt.go

package main

import (
    "io"
    "io/ioutil"
    "os"
    "strings"
)

// Reads all .txt files in the current folder
// and encodes them as strings literals in textfiles.go
func main() 
    fs, _ := ioutil.ReadDir(".")
    out, _ := os.Create("textfiles.go")
    out.Write([]byte("package main \n\nconst (\n"))
    for _, f := range fs 
        if strings.HasSuffix(f.Name(), ".txt") 
            out.Write([]byte(strings.TrimSuffix(f.Name(), ".txt") + " = `"))
            f, _ := os.Open(f.Name())
            io.Copy(out, f)
            out.Write([]byte("`\n"))
        
    
    out.Write([]byte(")\n"))

要将所有 .txt 文件编译到您的可执行文件中:

$ go generate
$ go build -o main

现在您的目录结构将如下所示:

main.go
main
scripts/includetxt.go
textfiles.go
a.txt
b.txt

textfiles.go 由 go generate 和 script/includetxt.go 生成的地方

textfiles.go

package main 

const (
a = `hello`
b = `world`
)

运行 main 会给出

$ ./main
hello
world

只要您对 UTF8 编码的文件进行编码,它就可以正常工作。如果您想对其他文件进行编码,您可以使用 go 语言(或任何其他工具)的全部功能来执行此操作。我使用这种技术将hex encodepng:s 转换为单个可执行文件。这需要对 includetxt.go 稍作改动。

【讨论】:

不错!我不知道该功能存在。这比在源代码中转储字符串文字要干净得多。 只是好奇,为什么我们必须在cmets中编写预处理器命令,然后单独运行go generatego build 注意到 //go:generate ... 评论时不能处理运行命令吗? 自 2019-07-23 起,此代码不适用于包含反引号的文本文件。它们要么产生语法错误,要么允许代码注入,这两者都不是我们所希望的。 我是新手,它通过运行主文件给了我未定义的a和b,includedtext.go文件已成功生成。 @Bunyk,我在Go Blog article 中找到了这个解释:最新的 Go 版本 1.4 包含一个新命令,可以更轻松地运行此类工具。它被称为 go generate,它通过扫描 Go 源代码中的特殊 cmets 来识别要运行的一般命令。重要的是要了解 go generate 不是 go build 的一部分。它不包含依赖分析,并且必须在运行 go build 之前显式运行。 它旨在供 Go 包的作者使用,而不是其客户端。【参考方案2】:

使用go-bindata。来自自述文件:

此工具可将任何文件转换为可管理的 Go 源代码。对...有用 将二进制数据嵌入到 Go 程序中。文件数据是可选的 gzip 在转换为原始字节切片之前进行压缩。

【讨论】:

现在有点过时了。生成代码没有通过 golint,并且 repo 的所有者没有与拉取请求交互。我会警惕这个图书馆 一个名为statik 的新项目似乎可以做到这一点,并且最近一直很活跃。 此工具似乎已迁移到新的专用项目 - github.com/go-bindata/go-bindata,但我的回答中提到了其他(并且可能更好)替代方案。【参考方案3】:

一直在寻找相同的东西并遇到了esc: Embedding Static Assets in Go(截至 2014 年 11 月 19 日),作者Matt Jibson 正在评估其他 3 个声称可以进行文件嵌入的流行包:

    rakyll/statik jteeuwen/go-bindata(以及新的官方go-bindata/go-bindata 和另一个改进的kevinburke/go-bindata) GeertJohan/go.rice

并解释为什么他最终想出了自己的包:

    mjibson/esc

所以在简单地尝试了所有这些(按顺序)之后,我自然而然地选择了 Matt 的 esc,因为它是唯一一个开箱即用的对我来说是必需的功能( HTTPS 服务在单个可执行文件中),即:

    可以获取一些目录并以与http.FileSystem 兼容的方式将所有文件递归地嵌入其中 可以选择禁用,以便在不更改客户端代码的情况下与本地文件系统一起使用以进行本地开发 不会在后续运行中更改输出文件在文件更改时具有合理大小的差异 能够通过//go:generate 完成工作,而不是强迫您手动编写额外的 Go 代码

#2 点对我来说很重要,但由于某种原因,其余的软件包效果不佳。

来自 esc 的自述文件:

esc 将文件嵌入到 go 程序中,并为它们提供 http.FileSystem 接口。

它在指定路径的命名目录下递归添加所有命名文件或文件。输出文件提供了一个 http.FileSystem 接口,对标准库之外的包的依赖为零。

【讨论】:

【参考方案4】:

您可以使用string literal 将文本定义为常量或变量。字符串文字是通过用反引号括起来的字符串来定义的。例如`字符串`。

例如:

package main

import "fmt"

func main() 
    const text = `
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit  
amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante 
hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet 
vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut 
libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, 
consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a 
semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. 
Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut 
convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis 
quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis 
parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae 
nisi at sem facilisis semper ac in est.
`

    fmt.Println(text)

【讨论】:

虽然更简单,但如果您的“文件”包含% 之类的字符,则它不起作用。【参考方案5】:

查看packr,使用起来非常友好

package main

import (
  "net/http"

  "github.com/gobuffalo/packr"
)

func main() 
  box := packr.NewBox("./templates")

  http.Handle("/", http.FileServer(box))
  http.ListenAndServe(":3000", nil)

【讨论】:

如果您将最多星级作为衡量软件的标准,那么这就是其中之一。 这将被pkger取代【参考方案6】:

使用 go1.16,您可以开始使用 embed,这是一个标准包,可帮助您将静态非 go 文件嵌入到二进制文件中

文档:https://pkg.go.dev/embed 示例:https://blog.carlmjohnson.net/post/2021/how-to-use-go-embed/

对于 go ,您可以使用 packr。这是一个很棒的工具,你可以在https://github.com/gobuffalo/packr查看更多信息

【讨论】:

【参考方案7】:

我使用了一个简单的函数来读取go generate 运行中的外部模板并从中生成 Go 代码。将生成一个将模板作为字符串返回的函数。然后可以使用tpl, err := template.New("myname").Parse(mynameTemplate())

解析返回的模板字符串

我确实将该代码放到了 github 上。你可能想试试https://github.com/wlbr/templify

非常简单,但对我来说效果很好。

【讨论】:

【参考方案8】:

基于@CoreyOgburn 评论和Github comment,创建了以下sn-p:

//go:generate statik -src=./html

package main

import (
    _ "./statik"
    "github.com/rakyll/statik/fs"
)

func statikFile() 
    s, _ := fs.New()
    f, _ := s.Open("/tmpl/login.html")
    b, _ := ioutil.ReadAll(f)
    t, _ := template.New("login").Parse(string(b))
    t.Execute(w, nil)

然后运行

go generate

随后

go build

应该创建一个包含文件的二进制文件

【讨论】:

现在我推荐packr,see this answer。

以上是关于如何将文件嵌入到 Go 二进制文件中的主要内容,如果未能解决你的问题,请参考以下文章

如何将附属程序集嵌入到 EXE 文件中

将 boost 二进制文件嵌入到 xcode 项目中

embed小技巧-动态文件更新

如何将文件映射到内存?

在类库中嵌入二进制文件

Xcode 中没有显示“嵌入式二进制文件”?