go打包静态资源
Go一般编译出来的可执行二进制文件都是单个的文件,非常适合复制和部署
在实际使用中,除了需要向部署的服务器上复制二进制文件,可能还需要一些配置文件或静态文件(如html模板、图片、CSS、javascript等),而且在复制时要注意保持开发时的目录结构和位置等信息
如果这些文件也能嵌入到二进制文件中,我们只需复制、执行单个的可执行文件即可,那就无需上述的额外操作了
一些开源的项目很久以前就开始做这方面的工作,比如gobuffalo/packr、markbates/pkger、rakyll/statik、knadh/stuffbin等等
直到2019年末一个提案(issue#35950)被提出,期望Go官方编译器支持嵌入静态文件。后来Russ Cox专门写了一个设计文档Go command support for embedded static assets, 并最终实现了它
embed是在Go 1.16中新加包,它通过//go:embed指令,在编译阶段将静态资源文件打包进编译好的程序中,并提供访问这些文件的能力
//go:embed指令
必须导入"embed"包,如果未使用embed.FS,使用空白导入:import _ "embed"
使用注释格式的//go:embed指令来使用
指令后面为要嵌入的静态资源名称,多个使用空格隔开,也可以使用多行重复添加
一行或多行指令的下一行必须紧跟着嵌入后的变量声明,之间只允许空白行和'//'行注释
//go:embed files or dirs //go:embed files or dirs var variable_name Type
镶嵌的资源路径
资源路径中的路径分隔符只能使用正斜杠"/",即使是在windows中
资源路径不能以"/"、"."、".."开头
如果资源路径中有空格,使用双引号(")或反引号(`)包含
资源路径如果为目录,会递归的嵌入该目录中所有的子目录及文件,但是会忽略以点(.)或下划线(\_)开头的文件
资源路径中可以使用星号(*)通配符,当使用通配符时不会忽略子目录中以点(.)或下划线(\_)开头的文件
//go:embed image 不会镶嵌image/.tmp之类的目录或文件 但是子目录下的此类文件不会忽略 //go:embed image/* 不会镶嵌image/.tmp之类的目录或文件 并且子目录中仍然会忽略此类目录或文件
添加all标识符后会镶嵌所有文件,包含以点(.)或下划线(\_)开头的文件
//go:all:image
镶嵌资源的变量
镶嵌资源的变量只能位于包级别声明的全局变量
Type为镶嵌的数据类型,只支持嵌入为string, []byte和embed.FS三种类型
当仅嵌入一个文件时,定义的变量类型只能为string或[]byte,值为该文件的内容
当嵌入多个文件、一个目录、多个目录时,定义的变量类型只能为embed.FS,使用io/fs包来实现
embed.FS数据类型
embed.FS是通过拓展了io/fs包的FS接口来实现的文件系统加载
我们在使用embed.FS经常会使用到io/fs包中的一些接口和函数
常用的io/fs包中的接口和函数
Sub包级函数
Sub(fsys FS, dir string) (FS, error) 用于创建一个fsys文件系统中的dir子目录的文件系统
FS文件系统接口
type FS interface { Open(name string) (File, error) }
FileInfo文件信息对象
type FileInfo interface { Name() string // 不含路径的文件名 Size() int64 // 如果是文件,返回字节为单位的大小; 如果不是文件则根据系统不同返回不同 Mode() FileMode // 文件的模式位 ModTime() time.Time // 修改时间 IsDir() bool // 是否为目录 Sys() any // 底层数据源(可能返回 nil) }
File 文件对象
type File interface { Stat() (FileInfo, error) Read(out []byte) (int, error) // 从文件中读取最多len(out)字节数据并写入out Close() error }
DirEntry 目录对象
type DirEntry interface { Name() string IsDir() bool Type() FileMode // 文件的模式位 Info() (FileInfo, error) // 获取文件信息对象 }
embed.FS常用方法
Open(file_name) (fs.File,err) 返回一个文件对象和错误对象 ReadFile(file_name) ([]byte,err) 返回[]byte格式的文件内容和错误对象 ReadDir(dir_name) ([]fs.DirEntry,error) 返回[]fs.DirEntry格式的目录对象数组和错误对象 该数组中包含文件夹的一层目录中的所有文件和目录
我们看到embed.FS的常用方法返回的都是io/fs中的数据类型,所以我们上面介绍了很多io/fs的接口和方法
embed.FS文件系统实例对象中的当前目录
我们在读取embed.FS文件系统时,传入的file_name或dir_name都是直接以文件名开始,不能以"/"、"."、".."开头
这类似于我们常说的相对与当前所在目录的相对目录,那么当前所在目录是哪个目录呢?
其实在FS文件系统初始化创建时,会先创建一个文件系统的根,类似于根目录的概念,并把该目录作为当前所在目录,我们无需知道这个目录名是什么
在后续我们镶嵌资源时,会将镶嵌的资源放入这个根目录中
现在我们假设这个根目录为root目录(实际上并不是) 当我们镶嵌了一个one.txt文件、一个two.jpg文件和一个img目录时 这个文件系统中的结构应该是 root/ ├── img/ │ ├── logo.png │ └── icon/ │ ├── help.svg │ └── error.svg ├── ont.txt └── two.jpg 此时我们在root中,也就是说当前所在目录为root目录 当我们打开ont.txt或者two.jpg时,直接输入名字即可 当我们打开img目录时,是需要添加img/前缀的,如img/logo.png
单个文件
定义的接收变量类型只能为string或[]byte
变量的值就是该文件的实际内容
import "embed" //go:embed demo.txt var data string 或者 var data []byte func main(){ fmt.Println(data) }
多个文件
定义的变量类型只能为embed.FS
要嵌入的多个静态资源名称使用空格隔开,也可以使用多行重复添加
import "embed" //go:embed demo.txt test.txt 或者分两行添加指令 //go:embed demo.txt //go:embed test.txt var data embed.FS func main(){ read, err := data.ReadFile("demo.txt") fmt.Println(string(read)) }
目录
定义的变量类型只能为embed.FS
import "embed" //go:embed dir var data embed.FS dir,_ := data.ReadDir("dir") for _,entry := range dir { // 抛弃索引 info, _ := entry.Info() // 获取文件的信息 file_name := "dir/" + info.Name() // 获取文件路径及名称 read,_ := data.ReadFile(file_name) fmt.Println(string(read)) }