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

2023年 10月 12日 58.7k 0

在许多时候我们开发的应用程序可能依赖一些资源文件,例如图片、静态网页等等,但是我们只想编译完成后得到一个单独的可执行文件应当怎么做呢?内嵌资源到可执行文件中就是一个很好地选择,事实上无论是C/C++还是C#都提供了内嵌资源的特性,当然Go也不例外。

Go语言本身自带的标准库embed就可以实现将资源文件内嵌至可执行文件中,这是Golang的1.16版本才开始支持的特性。

1,嵌入文件基本操作

通常嵌入单个文件并读取是非常简单的,我们使用//go:embed这个指令即可,例如现在我的工程目录下有个test.txt的文件:

image.png

现在我要将其嵌入至我编译后的Go语言可执行文件中去,并读取其中内容,以字符串形式,只需要写代码如下:

package main

import (
	_ "embed"
	"fmt"
)

// 将文件嵌入并读取为字符串

//go:embed test.txt
var embedText string

func main() {
	fmt.Println(embedText)
}

运行结果:

image.png

现在大家可以尝试使用go build命令将其编译成exe,并把这个exe拎到别的地方,你会发现它照样可以运行,因为此时test.txt已经嵌入到exe中去了!

可见实现文件嵌入的重点在这两行:

//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:embed test.txt
var embedBytes []byte

可见嵌入并读取文件是很简单的,既然能够读取到文件字节内容,那么如果想再把这个文件释放出来是不是也是很简单的事情呢?拿到了嵌入的文件内容字节切片后,借助bufio包的Writer结构体对象即可实现把嵌入文件释放出来,这里就不再赘述了。

2,嵌入文件并读取为embed.FS类型

上述是嵌入单个文件,是属于比较简单的情况,那么当我们要嵌入多个文件甚至是一个文件夹的时候,上述情景就不能满足我们了!

这时,我们可以将文件嵌入并读取为embed.FS类型,该类型是一个只读的存放嵌入的文件的容器,也可以通过//go:embed指令接收嵌入的文件。

(1) 嵌入单个文件

我们首先通过嵌入单个文件来学习一下embed.FS类型的基本使用:

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对象中:

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

结果:

image.png

可见这样可以同时嵌入多个文件,在//go:embed指令后接多个要嵌入的文件路径即可,多个文件路径之间使用空格隔开。

所以,我们完全就可以把embed.FS对象想象成一个文件夹,只不过它是个特殊的文件夹,它位于编译后的可执行文件内部。那么使用ReadFile函数读取文件时,也是指定读取这个内部的文件夹中的文件,上述我们使用//go:embed指令嵌入了两个文件,就可以视为这两个文件在编译时被放入到这个特殊的“文件夹”中去了,只不过文件放进去后文件名是不会改变的。

(3) 嵌入文件夹

除此之外,我们还可以嵌入一整个文件夹,假设现在我的工程目录下有如下文件:

image.png

现在将resource文件夹嵌入至可执行文件中:

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

结果:

image.png

可见我们成功地嵌入并读取到了文件内容。

嵌入文件夹时只需要指定//go:embed后面为文件夹即可,例如上述的//go:embed resource就将整个resource文件夹,包括这个文件夹本身都嵌入进去了。

需要注意的是:

  • 当指定嵌入文件夹时,该文件夹及其中所有的文件,都会递归地被嵌入可执行文件
  • 但是如果文件夹中包含有以.或者_开头的文件,这些文件就会被视为隐藏文件,会被排除,不会被嵌入
  • 我们还可以使用通配符形式嵌入文件夹,例如://go:embed resource/*,使用通配符形式时,隐藏文件也会被嵌入,并且文件夹本身也会被嵌入

可见无论是嵌入文件,还是文件夹,都是很好理解的,我们都可以理解为将文件或者文件夹放到embed.FS这个特殊的“文件夹”对象中去了,然后ReadFile读取文件时,就是在这个特殊的“文件夹”中读取嵌入的文件了,当然其参数指定的相对文件路径也是相对这个特殊的“文件夹”的根路径。

除了读取文件内容,还可以列出其中文件信息:

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 是否是文件夹:%vn", item.Name(), item.IsDir())
	}
}

结果:

image.png

ReadDir方法用于读取嵌入的文件夹中的文件信息,返回DirEntry切片和错误对象。

上述我们读取的是embed.FS对象中指定的文件夹下信息,如果我们直接想看一下embed.FS对象中嵌入的所有文件和文件夹呢?在ReadDir函数传入"."作为参数即可:

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 是否是文件夹:%vn", item.Name(), item.IsDir())
	}
}

结果:

image.png

到这里,相信大家更加能够理解了为什么embed.FS可以被比作一个特殊的文件夹了!

我们还可以编写一个函数,递归地查看embed.FS中所有文件并调用:

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, ".")
}

结果:

image.png

3,总结

可见Go语言嵌入资源文件事实上是很简单的,在同时嵌入多个文件时,如果能够理解embed.FS这个类型的对象是一个容器(即上述说的特殊的文件夹),那么就能够掌握它的用法了!

参考:

  • Go标准库文档embed:传送门

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论