Go实现内嵌文件到可执行文件(exe文件)中去

在许多时候我们开发的应用程序可能依赖一些资源文件,例如图片、静态网页等等,但是我们只想编译完成后得到一个单独的可执行文件应当怎么做呢?内嵌资源到可执行文件中就是一个很好地选择,事实上无论是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这个类型的对象是一个容器(即上述说的特殊的文件夹),那么就能够掌握它的用法了!

参考:

相关推荐
小信啊啊21 小时前
Go语言切片slice
开发语言·后端·golang
Victor3561 天前
Netty(20)如何实现基于Netty的WebSocket服务器?
后端
缘不易1 天前
Springboot 整合JustAuth实现gitee授权登录
spring boot·后端·gitee
Kiri霧1 天前
Range循环和切片
前端·后端·学习·golang
WizLC1 天前
【Java】各种IO流知识详解
java·开发语言·后端·spring·intellij idea
Victor3561 天前
Netty(19)Netty的性能优化手段有哪些?
后端
爬山算法1 天前
Netty(15)Netty的线程模型是什么?它有哪些线程池类型?
java·后端
白宇横流学长1 天前
基于SpringBoot实现的冬奥会科普平台设计与实现【源码+文档】
java·spring boot·后端
Python编程学习圈1 天前
Asciinema - 终端日志记录神器,开发者的福音
后端
bing.shao1 天前
Golang 高并发秒杀系统踩坑
开发语言·后端·golang