注册 登录

清河洛

go打包静态资源

qingheluo2024-07-02清河洛257
Go一般编译出来的可执行二进制文件都是单个的文件,非常适合复制和部署在实际使用中,除了需要向部署的服务器上复制二进制文件,可能还需要一些配置文件或静态文件(如html模板、图片、CSS、javascript等),而且在复制时要注意保持开发时的目录结构和位置等信息如果这些文件也能嵌入到二进制文件中,我们只需复制、执行单个的可执行文件即可,那就无需上述的额外操作了一些开源的项目很久以前就开始做这方面的工作,比如gobuffalo/packr、markbates/pkger、rakyll/statik、knadh/stuffbin等等直到2019年末一个提案(issue#35950)被提出,期...

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


网址导航