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

88

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

8个回答

136

===== 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脚本,该脚本读取所有文本文件,并将它们作为字符串文字输出到一个新文件中。为了缩短代码长度,我跳过了错误处理。

script/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
hello
world

只要您编码的是UTF8编码的文件,这将正常工作。如果您想编码其他文件,则可以利用go语言(或任何其他工具)的全部功能来实现。我使用此技术将png文件十六进制编码为单个可执行文件。这需要对includetxt.go进行微小更改。


1
太好了!我不知道这个函数的存在。这比在源代码中倾倒字符串字面量要干净得多。 - Nat Knight
4
为什么我们需要在注释中编写预处理器命令,然后单独运行 go generatego build 不能在注意到 //go:generate ... 注释时自动运行命令吗? - Bunyk
截止2019-07-23,该代码无法处理包含重音符号的文本文件。 它们会产生语法错误或允许进行代码注入,这两者都是不希望出现的。 - Roland Illig
2
我是Go新手,运行主文件后它给了我未定义的a和b,includedtext.go文件已经成功生成。 - Hasan A Yousef
1
@Bunyk,我在Go Blog文章中找到了以下解释:最新版本的Go 1.4包含一个新命令,可以更轻松地运行这些工具。它被称为go generate,并通过扫描Go源代码中标识通用命令的特殊注释来工作。重要的是要理解,go generate不是go build的一部分。它不包含任何依赖关系分析,必须在运行go build之前显式运行。 它旨在由Go软件包的作者使用,而不是其客户端。 - b01
官方文档建议使用下划线(空白标识符)导入嵌入包,即 import _ "embed",以便使用 go:embed 指令将内容加载到 string[]byte 类型的变量中。有关详细信息,请参见 github 上的讨论 - ufukty

32

使用 go-bindata 工具。从 README 文件中可以看到:

该工具可将任何文件转换为易于管理的 Go 源代码。它非常有用,可以将二进制数据嵌入到 Go 程序中。在转换为原始字节切片之前,文件数据可选择进行 gzip 压缩。


5
现在这个已经有些过时了。生成的代码无法通过golint检查,而且仓库的所有者没有回应拉取请求。我会对这个库保持警惕。 - kio
3
一个名为statik的新项目似乎可以做到这一点,并且最近一直很活跃。 - Corey Ogburn
1
这个工具似乎已经迁移到了新的专用项目 https://github.com/go-bindata/go-bindata,但是在我的回答中提到了其他可能更好的替代方案。 - Sevenate

13

更新于2023年10月19日

来自之前推荐的作者mjibson/esc

停止使用这个,现在切换到标准库中的embed

原始答案

我也在寻找同样的东西,偶然发现了esc: 在Go中嵌入静态资源(作者为Matt Jibson),他正在评估其他三个声称可以进行文件嵌入的流行包。

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

并解释他最终为什么要开发自己的包:

  1. mjibson/esc
所以在简短地尝试了它们之后(按照那个顺序),我自然而然地选择了Matt的esc,因为它是唯一一个能够直接使用(在一个可执行文件中)满足我需求的功能(HTTPS服务),具体包括:
  1. 能够接受一些目录,并以与http.FileSystem兼容的方式递归嵌入所有文件
  2. 可选择性地禁用,以便在本地开发时使用本地文件系统而无需更改客户端代码
  3. 在文件更改时不会改变输出文件,并且差异大小适中
  4. 能够通过//go:generate执行工作,而无需手动编写额外的Go代码
对我来说,#2点非常重要,而其他包由于某种原因都没有很好地满足我的需求。
从esc的README中可以看到:
esc将文件嵌入到Go程序中,并为它们提供http.FileSystem接口。
它会将所有指定的命名文件或以指定路径递归方式添加的文件添加到输出文件中。输出文件提供了一个http.FileSystem接口,对于标准库之外的包没有任何依赖。

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

1
如果你正在寻找衡量软件最受欢迎的标准,那么这就是它。 - Phani Rithvij
将被pkger取代。 - Sevenate

5

4
你可以使用字符串字面量来将文本定义为常量或变量。字符串字面量通过用反引号括起来来定义字符串。例如:`字符串`。
例如:
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)
}

1
虽然更简单,但如果您的“文件”包含像“%”这样的字符,则无法正常工作。 - Sridhar Ratnakumar

2

根据@CoreyOgburn的评论和这个Github评论,创建了以下代码片段:

//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,详见此答案 - 030

2
我使用了一个简单的函数在 go generate 运行时读取外部模板,并从中生成 Go 代码。将生成返回模板字符串的函数。然后可以使用 tpl, err := template.New("myname").Parse(mynameTemplate()) 解析返回的模板字符串。
我已经将该代码放到 Github 上。你可能想尝试一下:https://github.com/wlbr/templify 非常简单,但对我来说很有效。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接