Go编译的程序非常适合部署,如果没有通过CGO引用其它的库的话,我们一般编译出来的可执行二进制文件都是单个的文件,非常适合复制和部署。在实际使用中,除了二进制文件,可能还需要一些配置文件,或者静态文件,比如html模板、静态的图片、CSS、javascript等文件,如何这些文件也能打进到二进制文件中,那就太美妙,我们只需复制、按照单个的可执行文件即可。
一些开源的项目很久以前就开始做这方面的工作,比如gobuffalo/packr、markbates/pkger、rakyll/statik、knadh/stuffbin等等,但是不管怎么说这些都是第三方提供的功能,如果Go官方能内建支持就好了。2019末一个提案被提出issue#35950,期望Go官方编译器支持嵌入静态文件。后来Russ Cox专门写了一个设计文档Go command support for embedded static assets, 并最终实现了它。
Go 1.16中包含了go embed的功能,而且Go1.16基本在一个月左右的时间就会发布了,到时候你可以尝试使用它,如果你等不及了,你也可以下载Go 1.16beta1尝鲜。
本文将通过例子,详细介绍go embed的各个功能。
嵌入
- 对于单个的文件,支持嵌入为字符串和 byte slice
- 对于多个文件和文件夹,支持嵌入为新的文件系统FS
- 比如导入 "embed"包,即使无显式的使用
go:embed
指令用来嵌入,必须紧跟着嵌入后的变量名
- 只支持嵌入为string, byte slice和embed.FS三种类型,这三种类型的别名(alias)和命名类型(如type S string)都不可以
嵌入为字符串
比如当前文件下有个hello.txt的文件,文件内容为hello,world!
。通过go:embed
指令,在编译后下面程序中的s变量的值就变为了hello,world!
。
1 2 3 4 5 6 7 8 9 10 11 12 13
| package main import ( _ "embed" "fmt" ) var s string func main() { fmt.Println(s) }
|
嵌入为byte slice
你还可以把单个文件的内容嵌入为slice of byte,也就是一个字节数组。
1 2 3 4 5 6 7 8 9 10 11 12 13
| package main import ( _ "embed" "fmt" ) var b []byte func main() { fmt.Println(b) }
|
嵌入为fs.FS
甚至你可以嵌入为一个文件系统,这在嵌入多个文件的时候非常有用。
比如嵌入一个文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package main import ( "embed" "fmt" ) var f embed.FS func main() { data, _ := f.ReadFile("hello.txt") fmt.Println(string(data)) }
|
嵌入本地的另外一个文件hello2.txt, 支持同一个变量上多个go:embed
指令(嵌入为string或者byte slice是不能有多个go:embed
指令的):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package main import ( "embed" "fmt" ) var f embed.FS func main() { data, _ := f.ReadFile("hello.txt") fmt.Println(string(data)) data, _ = f.ReadFile("hello2.txt") fmt.Println(string(data)) }
|
当前重复的go:embed
指令嵌入为embed.FS是支持的,相当于一个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package main import ( "embed" "fmt" ) var f embed.FS func main() { data, _ := f.ReadFile("hello.txt") fmt.Println(string(data)) }
|
还可以嵌入子文件夹下的文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package main import ( "embed" "fmt" ) var f embed.FS func main() { data, _ := f.ReadFile("p/hello.txt") fmt.Println(string(data)) data, _ = f.ReadFile("p/hello2.txt") fmt.Println(string(data)) }
|
还可以支持模式匹配的方式嵌入,下面的章节专门介绍。
同一个文件嵌入为多个变量
比如下面的例子,s和s2变量都嵌入hello.txt的文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package main import ( _ "embed" "fmt" ) var s string var s2 string func main() { fmt.Println(s) fmt.Println(s2) }
|
exported/unexported的变量都支持
Go可以将文件可以嵌入为exported的变量,也可以嵌入为unexported的变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package main import ( _ "embed" "fmt" ) var s string var S string func main() { fmt.Println(s) fmt.Println(S) }
|
package级别的变量和局部变量都支持
前面的例子都是package一级的的变量,即使是函数内的局部变量,也都支持嵌入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package main import ( _ "embed" "fmt" ) func main() { var s string var s2 string fmt.Println(s, s2) }
|
局部变量s的值在编译时就已经嵌入了,而且虽然s和s2嵌入同一个文件,但是它们的值在编译的时候会使用初始化字段中的不同的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 0x0021 00033 (/Users/....../main.go:10) MOVQ "".embed.1(SB), AX 0x0028 00040 (/Users/....../main.go:10) MOVQ "".embed.1+8(SB), CX 0x002f 00047 (/Users/....../main.go:13) MOVQ "".embed.2(SB), DX 0x0036 00054 (/Users/....../main.go:13) MOVQ DX, "".s2.ptr+72(SP) 0x003b 00059 (/Users/....../main.go:13) MOVQ "".embed.2+8(SB), BX ...... "".embed.1 SDATA size=16 0x0000 00 00 00 00 00 00 00 00 0d 00 00 00 00 00 00 00 ................ rel 0+8 t=1 go.string."hello, world!"+0 "".embed.2 SDATA size=16 0x0000 00 00 00 00 00 00 00 00 0d 00 00 00 00 00 00 00 ................ rel 0+8 t=1 go.string."hello, world!"+0
|
注意s和s2的变量的值是在编译期就确定了,即使在运行时你更改了hello.txt的文件,甚至把hello.txt都删除了也不会改变和影响s和s2的值。
只读
嵌入的内容是只读的。也就是在编译期嵌入文件的内容是什么,那么在运行时的内容也就是什么。
FS文件系统值提供了打开和读取的方法,并没有write的方法,也就是说FS实例是线程安全的,多个goroutine可以并发使用。
1 2 3 4
| type FS func (f FS) Open(name string) (fs.File, error) func (f FS) ReadDir(name string) ([]fs.DirEntry, error) func (f FS) ReadFile(name string) ([]byte, error)
|
go:embed指令
go:embed指令支持嵌入多个文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package main import ( "embed" "fmt" ) var f embed.FS func main() { data, _ := f.ReadFile("hello.txt") fmt.Println(string(data)) data, _ = f.ReadFile("hello2.txt") fmt.Println(string(data)) }
|
当然你也可以像前面的例子一样写成多行go:embed
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package main import ( "embed" "fmt" ) var f embed.FS func main() { data, _ := f.ReadFile("hello.txt") fmt.Println(string(data)) data, _ = f.ReadFile("hello2.txt") fmt.Println(string(data)) }
|
支持文件夹
文件夹分隔符采用正斜杠/
,即使是windows系统也采用这个模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package main import ( "embed" "fmt" ) var f embed.FS func main() { data, _ := f.ReadFile("p/hello.txt") fmt.Println(string(data)) data, _ = f.ReadFile("p/hello2.txt") fmt.Println(string(data)) }
|
使用的是相对路径
相对路径的根路径是go源文件所在的文件夹。
支持使用双引号"
或者反引号的方式应用到嵌入的文件名或者文件夹名或者模式名上,这对名称中带空格或者特殊字符的文件文件夹有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package main import ( "embed" "fmt" ) var f embed.FS func main() { data, _ := f.ReadFile("he llo.txt") fmt.Println(string(data)) }
|
匹配模式
go:embed
指令中可以只写文件夹名,此文件夹中除了.
和_
开头的文件和文件夹都会被嵌入,并且子文件夹也会被递归的嵌入,形成一个此文件夹的文件系统。
如果想嵌入.
和_
开头的文件和文件夹, 比如p文件夹下的.hello.txt文件,那么就需要使用*
,比如go:embed p/*
。
*
不具有递归性,所以子文件夹下的.
和_
不会被嵌入,除非你在专门使用子文件夹的*
进行嵌入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package main import ( "embed" "fmt" ) var f embed.FS func main() { data, _ := f.ReadFile("p/.hello.txt") fmt.Println(string(data)) data, _ = f.ReadFile("p/q/.hi.txt") fmt.Println(string(data)) }
|
嵌入和嵌入模式不支持绝对路径、不支持路径中包含.
和..
,如果想嵌入go源文件所在的路径,使用*
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package main import ( "embed" "fmt" ) var f embed.FS func main() { data, _ := f.ReadFile("hello.txt") fmt.Println(string(data)) data, _ = f.ReadFile(".hello.txt") fmt.Println(string(data)) }
|
文件系统
embed.FS
实现了 io/fs.FS
接口,它可以打开一个文件,返回fs.File
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package main import ( "embed" "fmt" ) var f embed.FS func main() { helloFile, _ := f.Open("hello.txt") stat, _ := helloFile.Stat() fmt.Println(stat.Name(), stat.Size()) }
|
它还提供了ReadFileh和ReadDir功能,遍历一个文件下的文件和文件夹信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package main import ( "embed" "fmt" ) var f embed.FS func main() { dirEntries, _ := f.ReadDir("p") for _, de := range dirEntries { fmt.Println(de.Name(), de.IsDir()) } }
|
因为它实现了io/fs.FS
接口,所以可以返回它的子文件夹作为新的文件系统:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package main import ( "embed" "fmt" "io/fs" "io/ioutil" ) var f embed.FS func main() { ps, _ := fs.Sub(f, "p") hi, _ := ps.Open("q/hi.txt") data, _ := ioutil.ReadAll(hi) fmt.Println(string(data)) }
|
应用
net/http
先前,我们提供一个静态文件的服务时,使用:
1
| http.Handle("/", http.FileServer(http.Dir("/tmp")))
|
现在,io/fs.FS
文件系统也可以转换成http.FileServer的参数了:
1 2 3 4 5
| type FileSystem func FS(fsys fs.FS) FileSystem type Handler func FileServer(root FileSystem) Handler
|
所以,嵌入文件可以使用下面的方式:
1
| http.Handle("/", http.FileServer(http.FS(fsys)))
|
text/template和html/template.
同样的,template也可以从嵌入的文件系统中解析模板:
1 2
| func ParseFS(fsys fs.FS, patterns ...string) (*Template, error) func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error)
|