//go:embed 入门
涛叔Go1.16 引入了//go:embed
功能,可以将资源文件内容直接打包到二进制文件,方便部署。最近读到 Carl M. Johnson 写的 How to Use //go:embed 一文,写得相当不错。于是联系作者拿到了授权,现译成中文,分享给大家。
我之前写过 how to use //go:generate
一文介绍。通过//go:generate
,我们可以在编译之前通过自动运行一些工具来生成代码。现在 Go 有了新功能,好多之前生成代码的地方现在已经用不到了。除了这个逆天的 flag.Func feature新特性(因为这是作者提出并实现的。译者注),Go 1.16还引入了新的 //go:embed
编译指令。 它可以将任何文件或者文件夹的内容打包到编译出的可执行文件中。为了给大家演示 //go:embed
的功能,我创建了一个示例仓库。下面我来详细介绍各个示例。
简单来说,我们可以给代码添加一行特殊的注释来,Go 编译器知道是要嵌入文件还是嵌入文件夹。注释长得像 //go:embed 文件名
。注释占一行,紧接着一行是一个变量。如果要嵌入文件,变量的类型得是 string
或者 []byte
,如果要嵌入一组文件,变量的类型得是embed.FS
。
go:embed
指令可以识别 Go 的文件通配符,所以你可以写 files/*.html
来嵌入 files 下的所有 html 文件(但是不支持递归通配符 **/*.html
)。
更多技术细节请参考官方文档。接下来我们就通过几个示例来讲解 embed 可能的玩法。
版本信息
我在之前讲 //go:generate
的文章中跟大家介绍了如何通过 Go 链接器来给代码嵌入版本自信。//go:embed
则为我们提供了一种更简单的方法来从 version.txt 文本文件中提取版本信息:
package main
import (
"embed"
_ "fmt"
"strings"
)
var (
string = strings.TrimSpace(version)
Version //go:embed version.txt
string
version )
func main() {
.Printf("Version %q\n", Version)
fmt}
下面是一个更加复杂的例子,我们甚至可以根据编译 tag 的不同动态地嵌入不同的版本信息。
// version_dev.go
// +build !prod
package main
var version string = "dev"
// version_prod.go
// +build prod
package main
import (
"embed"
_ )
//go:embed version.txt
var version string
go run .
$ "dev"
Version
go run -tags prod .
$ "0.0.1" Version
Quine
Quine 是一种可以输出自身源码的程序。利用 go:embed
我们可以轻松实现 quine 程序:
package main
import (
"embed"
_ "fmt"
)
//go:embed quine.go
var src string
func main() {
.Print(src)
fmt}
运行程序,会输出自身源码。
嵌入复杂结构
如果我们有一些非常复杂但又要提前算出来的信息,我们可以把它们写到一个文件,放入项目,然后通过 //go:generate
和模板自动生成对应的 .go 文件(可以参考我之前写的 //go:generate
文章)。我们也可以将数据按照 Go 语言能识别的方式序列化并保存,然后在程序启动的时候加载并反解:
package main
import (
"bytes"
"embed"
_ "encoding/gob"
"fmt"
)
var (
// 文件 value.gob 保存了一些复杂的数据,是我们事先算好并保存的。
//go:embed value.gob
[]byte
b = func() (s struct {
s float64
Number string
Weather []string
Alphabet }) {
:= gob.NewDecoder(bytes.NewReader(b))
dec if err := dec.Decode(&s); err != nil {
panic(err)
}
return
}()
)
func main() {
.Printf("s: %#v\n", s)
fmt}
网站文件
这应该是 //go:embed
最主要的应用场景之一了。我们现在可以把网站所有的静态文件或者模板文件嵌入到一个可执行文件中。我们甚至还可以能过命令行参数在读磁盘文件和读内嵌文件之间进行转换:
package main
import (
"embed"
"io/fs"
"log"
"net/http"
"os"
)
func main() {
:= len(os.Args) > 1 && os.Args[1] == "live"
useOS .Handle("/", http.FileServer(getFileSystem(useOS)))
http.ListenAndServe(":8888", nil)
http}
//go:embed static
var embededFiles embed.FS
func getFileSystem(useOS bool) http.FileSystem {
if useOS {
.Print("using live mode")
logreturn http.FS(os.DirFS("static"))
}
.Print("using embed mode")
log, err := fs.Sub(embededFiles, "static")
fsysif err != nil {
panic(err)
}
return http.FS(fsys)
}
注间我们需要使用中 fs.Sub
来去除 embed.FS
的目录前缀,这样其行为才能跟 os.DirFS
保持一致。
Ben Johnson 也已弘发布了一个工具包,可以让 embed.FS
按文件内容的哈希值生成文件名。
下面是一个嵌入模板的示例:
package main
import (
"embed"
"os"
"text/template"
)
//go:embed *.tmpl
var tpls embed.FS
func main() {
:= "en.tmpl"
name if len(os.Args) > 1 {
= os.Args[1] + ".tmpl"
name }
:= "World"
arg if len(os.Args) > 2 {
= os.Args[2]
arg }
, err := template.ParseFS(tpls, "*")
tif err != nil {
panic(err)
}
if err = t.ExecuteTemplate(os.Stdout, name, arg); err != nil {
panic(err)
}
}
假设 en.tmpl
的内容是 Hello {{ . }}, how are you today?
,再假设 jp.tmpl
的内容是 こんにちは{{ . }}。お元気ですか。
,我们会看到如下输出内容:
$ go run ./main.go
Hello World, how are you today?
$ go run ./main.go jp ワールド
こんにちはワールド。お元気ですか。
Gotchas
常见问题
在使用 embed 的时候需要注意一些常见问题。首先,在使用 embed 指令的时候需要导入 embed
包。所以像下面这样的写法是不会生效的:
package main
import (
"fmt"
)
//go:embed file.txt
var s string
func main() {
.Print(s)
fmt}
go run missing-embed.go
$ -line-arguments
# command./missing-embed.go:8:3: //go:embed only allowed in Go files that import "embed"
另一方面,Go 又不允许导入没有引用的包。如果相导入一个包但又不引用它,你需要写成 import _ "embed"
,告诉 go 没有用到也要导入。
其次,你只能给包一级的变量添加 //go:embed
注释。给局部变量添加这种注释会报编译错误:
package main
import (
"embed"
_ "fmt"
)
func main() {
//go:embed file.txt
var s string
.Print(s)
fmt}
go run bad-level.go
$ -line-arguments
# command./bad-level.go:9:4: go:embed cannot apply to var inside func
最后,当你要嵌入文件假时,系统会自动掉队以 .
或者 _
开头的文件。但如果你使用了像 dir/*
这类的通配符,系统会嵌入所有匹配到的文件。即使是以 .
或者 _
开头的也不例外。注意了,如果你想嵌入一些文件但又不想公开所有文件列表的时候,在 macos 上不滤嵌入 .DS_Store
可能会有安全问题。因此,使用 //go:embed dir/\*
基本都有问题的,应该根据需要使用 //go:embed dir
或者 //go:embed dir/*.ext
。出于安全考虑,Go 不支持嵌入文件链接内容和父级目录内容。
文件嵌入应该还有各种千奇百怪的用法。比如说,你是不是可以把文档和许可信息嵌入命令行程序,这样就不需要阅读源码仓库的 README 文件了。可不可以将数据库查询写入一个 .sql
文件再嵌入程序。或者你可以实现一个分层文件系统,将通过 embed.FS
嵌入的内容和用户有提供的内容组合起来……这只是几个抛砖引玉的点子。我相信以后还会有更多巧妙而且想不到的用法。
Go 1.16 于2021年2月16日正式发布。如果你还没升级,今天赶紧试试吧。