在许多时候我们开发的应用程序可能依赖一些资源文件,例如图片、静态网页等等,但是我们只想编译完成后得到一个单独的可执行文件应当怎么做呢?内嵌资源到可执行文件中就是一个很好地选择,事实上无论是C/C++还是C#都提供了内嵌资源的特性,当然Go也不例外。
Go语言本身自带的标准库embed
就可以实现将资源文件内嵌至可执行文件中,这时Golang的1.16
版本才开始支持的特性。
1,嵌入文件基本操作
通常嵌入单个文件并读取是非常简单的,我们使用//go:embed
这个指令即可,例如现在我的工程目录下有个test.txt
的文件:
现在我要将其嵌入至我编译后的Go语言可执行文件中去,并读取其中内容,以字符串形式,只需要写代码如下:
go
package main
import (
_ "embed"
"fmt"
)
// 将文件嵌入并读取为字符串
//go:embed test.txt
var embedText string
func main() {
fmt.Println(embedText)
}
运行结果:
现在大家可以尝试使用go build
命令将其编译成exe
,并把这个exe
拎到别的地方,你会发现它照样可以运行,因为此时test.txt
已经嵌入到exe
中去了!
可见实现文件嵌入的重点在这两行:
go
//go:embed test.txt
var embedText string
这两行的意思就是:在编译代码时,将当前目录下的test.txt
文件嵌入至可执行文件中,并读取为字符串形式赋值给embedText
这个变量,可见嵌入文件的操作还是很简单的。
这里有以下注意事项:
//go:embed
是Go语言中的指令 ,看起来很像注释但是并非是注释,其中//
和go:embed
两者之间不能有空格,必须挨在一起//go:embed
后面接要嵌入的文件路径,以相对路径形式声明文件路径,文件路径和//go:embed
指令之间相隔一个空格,这里文件相对路径相对的是当前源代码文件的路径 ,并且这个路径不能 以/
或者./
开头- 必须要导入
embed
包才能够使用//go:embed
指令 - 上述
embedText
变量位于//go:embed
指令下方,表示这个变量用于接收并存放嵌入的文件的内容,以字符串形式 embedText
作为接收嵌入的文件的内容的变量,必须是全局变量,而不能是函数中的局部变量
同样地,如果嵌入的是二进制文件呢?那么可以读取为字节切片的形式,将上述string
换成[]byte
即可:
go
//go:embed test.txt
var embedBytes []byte
可见嵌入并读取文件是很简单的,既然能够读取到文件字节内容,那么如果想再把这个文件释放出来是不是也是很简单的事情呢?拿到了嵌入的文件内容字节切片后,借助bufio
包的Writer
结构体对象即可实现把嵌入文件释放出来,这里就不再赘述了。
2,嵌入文件并读取为embed.FS
类型
上述是嵌入单个文件,是属于比较简单的情况,那么当我们要嵌入多个文件甚至是一个文件夹的时候,上述情景就不能满足我们了!
这时,我们可以将文件嵌入并读取为embed.FS
类型,该类型是一个只读的存放嵌入的文件的容器,也可以通过//go:embed
指令接收嵌入的文件。
(1) 嵌入单个文件
我们首先通过嵌入单个文件来学习一下embed.FS
类型的基本使用:
go
package main
import (
"embed"
"fmt"
)
// 嵌入文件并作为embed.FS类型
//go:embed test.txt
var embedFile embed.FS
func main() {
// 读取嵌入的文件,返回字节切片
content, err := embedFile.ReadFile("test.txt")
if err != nil {
fmt.Println("读取文件错误!", err)
return
}
// 将读取到的字节切片转换成字符串输出
fmt.Println(string(content))
}
可见指令部分并不需要改,将接收变量类型改成embed.FS
即可,上述代码同样实现了嵌入文件的效果。
可见通过embed.FS
对象的ReadFile
方法,即可读取指定的嵌入的文件的内容,参数为嵌入的文件名,返回读取到的文件内容(byte
切片形式)和错误对象。
(2) 嵌入多个文件
我们可以一次性指定多个文件嵌入并存放到embed.FS
对象中:
go
package main
import (
"embed"
"fmt"
)
// 嵌入多个文件并作为embed.FS类型
// 将当前目录下test.txt和demo.txt嵌入至可执行文件,并存放到embed.FS对象中
//
//go:embed test.txt demo.txt
var embedFiles embed.FS
func main() {
// 读取嵌入的文件,返回字节切片
testContent, _ := embedFiles.ReadFile("test.txt")
demoContent, _ := embedFiles.ReadFile("demo.txt")
// 将读取到的字节切片转换成字符串输出
fmt.Println(string(testContent))
fmt.Println(string(demoContent))
}
结果:
可见这样可以同时嵌入多个文件,在//go:embed
指令后接多个要嵌入的文件路径即可,多个文件路径之间使用空格隔开。
所以,我们完全就可以把embed.FS
对象想象成一个文件夹 ,只不过它是个特殊的文件夹,它位于编译后的可执行文件内部。那么使用ReadFile
函数读取文件时,也是指定读取这个内部的文件夹中的文件,上述我们使用//go:embed
指令嵌入了两个文件,就可以视为这两个文件在编译时被放入到这个特殊的"文件夹"中去了,只不过文件放进去后文件名是不会改变的。
(3) 嵌入文件夹
除此之外,我们还可以嵌入一整个文件夹,假设现在我的工程目录下有如下文件:
现在将resource
文件夹嵌入至可执行文件中:
go
package main
import (
"embed"
"fmt"
)
// 嵌入一整个文件夹,作为embed.FS类型
//go:embed resource
var embedDir embed.FS
func main() {
// 读取嵌入的文件
a, _ := embedDir.ReadFile("resource/a.txt")
fmt.Println(string(a))
c, _ := embedDir.ReadFile("resource/dir/c.txt")
fmt.Println(string(c))
}
结果:
可见我们成功地嵌入并读取到了文件内容。
嵌入文件夹时只需要指定//go:embed
后面为文件夹即可,例如上述的//go:embed resource
就将整个resource
文件夹,包括这个文件夹本身都嵌入进去了。
需要注意的是:
- 当指定嵌入文件夹时,该文件夹及其中所有的文件,都会递归地被嵌入可执行文件
- 但是如果文件夹中包含有以
.
或者_
开头的文件,这些文件就会被视为隐藏文件,会被排除,不会被嵌入 - 我们还可以使用通配符形式嵌入文件夹,例如:
//go:embed resource/*
,使用通配符形式时,隐藏文件也会被嵌入,并且文件夹本身也会被嵌入
可见无论是嵌入文件,还是文件夹,都是很好理解的,我们都可以理解为将文件或者文件夹放到embed.FS
这个特殊的"文件夹"对象中去了,然后ReadFile
读取文件时,就是在这个特殊的"文件夹"中读取嵌入的文件了,当然其参数指定的相对文件路径也是相对这个特殊的"文件夹"的根路径。
除了读取文件内容,还可以列出其中文件信息:
go
package main
import (
"embed"
"fmt"
)
// 嵌入一整个文件夹,作为embed.FS类型
//go:embed resource
var embedDir embed.FS
func main() {
// 读取嵌入的embed.FS中的文件夹信息
items, e := embedDir.ReadDir("resource")
if e != nil {
fmt.Println("读取错误!", e)
return
}
// 遍历输出信息
for _, item := range items {
fmt.Printf("文件名:%s 是否是文件夹:%v\n", item.Name(), item.IsDir())
}
}
结果:
ReadDir
方法用于读取嵌入的文件夹中的文件信息,返回DirEntry
切片和错误对象。
上述我们读取的是embed.FS
对象中指定的文件夹下信息,如果我们直接想看一下embed.FS
对象中嵌入的所有文件和文件夹呢?在ReadDir
函数传入"."
作为参数即可:
go
package main
import (
"embed"
"fmt"
)
// 嵌入一整个文件夹,作为embed.FS类型
//go:embed resource
var embedDir embed.FS
func main() {
// 读取嵌入的embed.FS中的文件夹信息
items, e := embedDir.ReadDir(".")
if e != nil {
fmt.Println("读取错误!", e)
return
}
// 遍历输出信息
for _, item := range items {
fmt.Printf("文件名:%s 是否是文件夹:%v\n", item.Name(), item.IsDir())
}
}
结果:
到这里,相信大家更加能够理解了为什么embed.FS
可以被比作一个特殊的文件夹了!
我们还可以编写一个函数,递归地查看embed.FS
中所有文件并调用:
go
package main
import (
"embed"
"fmt"
)
// 嵌入一整个文件夹,作为embed.FS类型
//go:embed resource
var embedDir embed.FS
// 递归列出embed.FS中所有文件路径
func listEmbedFile(fs embed.FS, dir string) {
// 列出当前指定的嵌入的文件夹中的文件列表
list, _ := fs.ReadDir(dir)
// 遍历
for _, item := range list {
// 处理路径
path := ""
if dir != "." {
path = dir + "/"
}
// 如果是文件,输出路径
if !item.IsDir() {
fmt.Println(path + item.Name())
} else {
// 如果是目录,则进行递归操作
listEmbedFile(fs, path+item.Name())
}
}
}
func main() {
// 调用递归函数
listEmbedFile(embedDir, ".")
}
结果:
3,总结
可见Go语言嵌入资源文件事实上是很简单的,在同时嵌入多个文件时,如果能够理解embed.FS
这个类型的对象是一个容器(即上述说的特殊的文件夹),那么就能够掌握它的用法了!
参考:
- Go标准库文档
embed
:传送门