Golang嵌入静态文件embed(转载)

1. 问题

go1.16之前不使用第三方包前提下实现如下功能是比较困难的

编译后的二进制文件和ini/toml/yaml格式的配置文件必须同时存在,仅移动二进制文件可能就跑不起来了

开发一个简单的http服务,引入了js、css、html文件最终需要与go源码编译后的二进制文件保证正确的文件路径结构,然后一起压缩成一个文件后才能方便不出篓子的四处mv、cp

数据库迁移文件需要与编译后的二进制文件同时移动,不然mv到其他机器迁移命令直接跑不起来

以上问题本质上是因为go源码编译后的二进制可执行文件正常运行起来依赖了其他静态文件。需求核心就是最终编译生成的单个二进制可执行文件内嵌包含这些被依赖的静态文件。

严格意义上这些也谈不上问题,解决方案也有很多:docker、makeFile乃至人肉README.MD文档写清写明白都可以解决。只不过有没有一个方案能够一个二进制文件通行天下呢?go1.16之前这个问题有人思考过并且造出了各种轮子,也就是以第三方包形式存在各种花式嵌入静态文件至最终编译的二进制文件中。

go自1.16起官方内置了embed包以实现将静态文件直接嵌入到最终编译的二进制文件中,真正实现编译后可一个二进制文件行天下。自此其他花样实现的非官方嵌入静态文件至最终编译后二进制的第三方包的实现方式没落,颇有一番收编梁山好汉的意味。

2. embed包

官方文档:https://pkg.go.dev/embed

//go:embed 指令

以单独实体文件存在的文件go代码去访问无非就是使用一些io操作方法:按路径打开文件、读取文件、使用文件内容。既然原先以单个文件形式存在的各种形式的实体文件被嵌入到了最终编译好的单个二进制文件中了,原先的读取使用方式自然就不适用了,那么在代码中如何去访问这些被嵌入的文件呢?

embed包提供了一套简化抽象的文件嵌入语法指令://go:embed hello.txt ,所谓语法就是你按官方语法规则去写,编译打包后语法指定的要嵌入的文件就被嵌入到编译后的二进制可执行文件中去了,对于普适性的开发者而言不需要关注这些静态文件是如何嵌入进去的。

2.1. 源码实例

import _ "embed"

//go:embed hello.txt
var s string

文件结构如下:

.
├── hello.go
└── hello.txt

//go:embed指令以双斜杠开头,地球人都知道源码中双斜杠开头的是注释段。embed通过包含特定含义的注释语句指定要嵌入的文件,然后紧跟随一个go变量,go代码其他位置就可以通过这个变量访问这个文件了,既然是go变量自然有类型。支持的类型仅有3种:string、[]byte、embed.FS,虽用了仅字,其实已经绰绰有余,因为传统io操作库最终读取的单独存在的实体文件在go层面也只有这3种类型,当然embed.FS是fs.FS接口类型的实现。

显然string和[]byte嵌入后指向的是单个文件,用脚都能想到如果一个string和一个[]byte类型的go变量指向了多个文件,你如何从变量本身去区分这多个文件,而embed.FS是嵌入了一组文件,使用了fs.FS接口的实现,当然一组文件并不意味着绝对是多个文件,你也可以只有一个文件。

当导入单个文件时,注意这一行这里,也就是import _ "embed",众所周知import语句中下划线是没有显式使用包内方法、变量时又要导入包的语法规则,记住啦官方管它叫blank import,其实它是执行了对应导入包的init方法,告诉go编译器帮我们完成文件嵌入到编译后的二进制可执行文件中。

2.2. 文件组嵌入路径规则

嵌入一组文件的路径规则如何写?

显然string和[]byte两种类型只能嵌入一个文件,那么//go:embed PATH后方的PATH要精确指定到单个文件,没有什么好说的。

那么embed.FS嵌入一组文件呢?//go:embed PATH后方的PATH写法就有讲究了。说有讲究其实也没什么,搞清楚具体原则也轻松化解。具体而言有如下多种:

空格分隔的多个文件,多个文件成为一组,每个文件本身是指定到具体的路径的,写法查看这里。
符合path.Match规则的路径,且是相对路径,为什么说是相对路径呢,因为//go:embed PATH后方的PATH不允许以.、..、/开头。
那么主要就是理解path.Match规则了,语法规则描述在此,一看各种名字就懵逼了,其实简单的理解就是指定一个匹配规则用于描述path路径,规则本身也不是新创而且尽量控制了范围,只有*、?和[]三组,且与传统pcre正则没有多大区别。

path.Match规则

举个例子:下述文件结构path.Match规则用于指定嵌入的文件写法。

.
├── 12ab.txt
├── a.txt
├── aa.txt
├── aab.txt
├── ab.txt
├── ab34.txt
├── abc.txt
├── b.txt
├── b-.txt
├── ba.txt
├── contain_sub_dir.go
├── stub
│       └── single.txt
├── stubs
│       └── hello.txt
└── world.txt

在线源码在这里:https://github.com/jjonline/study_golang/tree/master/go_embed/three

路径描述规则在这里:https://pkg.go.dev/embed#hdr-Directives

  1. //go:embed ./ab34.txt 报错:Path must not contain '.' or '..' path elements nor begin with a leading slash,这是错误的例子。
  2. /go:embed [de]?.txt 正确写法,但是没有匹配到任何文件,所以编译报错pattern [de]?.txt: no matching files found,为什么呢?因为path.Match规则中[de]表示前1个字符必须是这d、e这两个字符集合的中某一个,文件列表中并没有以d或e开头的后后缀为txt的文件。
  3. //go:embed [ab].txt匹配什么呢?答案是:a.txt、b.txt,中括号括起来的字符与正则含义比较类似,集合中的某1个。
  4. //go:embed [ab]?.txt匹配什么呢?答案是:aa.txt、ab.txt、ba.txt、b-.txt,因为中括号的字符限定只能a和b,那么文件名开头只能是a或b,然后?匹配单个非分隔符的任意字符,分隔符是啥呢,就是斜杠/,那么这个规则就限定了不含后缀的文件名只能是两个字符;需要注意的是在某些正则中?表示可空的任意字符,path.Match规则中?是任意非空字符,也就是空格不算,所以a.txt、b.txt并不会被该规则包含。
  5. //go:embed [ab]*.txt匹配什么呢?答案是以a或b开头的所有后缀为txt的文件,因为*匹配一个或多个非分隔符的任意字符。
  6. //go:embed a.txt b.txt匹配什么呢?其实就是匹配两个确定的文件a.txt和b.txt,//go:embed指令是允许空格分隔指定多个匹配规则的。

然后直接罗列规则:

//go:embed指令纵使文件匹配语法书写没有问题,要是不存在对应的文件编译时也会报错。例如://go:embed a.txt b.txt c.txt写法上没有任何问题,但是因为c.txt不存在编译不通过。

嵌入的文件不支持文件的软连接,但是支持硬连接,linux文件系统的软连、硬连不再赘述;因为本质上硬连接只是文件的一个拷贝,而软连不是。

2.3. 搭配使用嵌入文件

嵌入的文件如何搭配使用?

这个问题其实已经回答过了,string、[]byte两种类型无需赘言。主要是embed.FS,这个一笔带过即可。

net/http包提供了适配fs.Fs文件系统http.FS函数,搭配net/http包的webServer读取嵌入到编译后二进制文件中的静态文件跟普通读取文件没有啥区别。

html/template提供了template.ParseFS方法将fs.Fs文件系统转换为template.Template的函数。

举一反三,其实本质还是得益于go抽象的fs.Fs文件系统,其实fs.Fs与embed均是go1.16开始实现的,这一块儿已经有介绍不再赘述:https://blog.jjonline.cn/golang/250.html

这里有一个gin如何使用embed的实际例子:https://github.com/jjonline/serve-swagger-ui/blob/88b72a64f6694ea93dd61750cb1318c59df187dd/stubs/enbed.go#L17

3. 参考

发表评论

邮箱地址不会被公开。 必填项已用*标注