在 Go 语言中,embed 包为程序提供了访问嵌入文件的功能,该功能是从 Go1.16 版本引进的,embed 包使得在编译时将文件嵌入到程序中变得非常方便。这个功能特别适用于需要将静态文件(如 HTML、CSS、图片等)打包到程序中的场景。本文将详细介绍 embed 包的使用方法,并提供相关示例代码来帮助理解。

1. 基础概念:Golang embed 是什么?

Go 语言的 embed 包允许我们使用 //go:embed 指令将文件的内容嵌入到 Go 程序中,注意: //go:embed之间是不能有空格的。嵌入的文件可以是字符串、字节切片或文件系统(embed.FS)。这些文件会在编译时被读取并存储在程序中,因此在运行时不需要外部文件的支持。

1.1 嵌入文件到字符串

要将文件的内容嵌入到字符串中,可以使用 string 类型。例如:

package main

import _ "embed"
import "fmt"

//go:embed hello.txt
var s string

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

在这个例子中,hello.txt 文件的内容会被嵌入到变量 s 中,并可以通过 fmt.Println 打印出来。

1.2 嵌入文件到字节切片

同样地,可以将文件的内容嵌入到字节切片中:

package main

import _ "embed"
import "fmt"

//go:embed hello.txt
var b []byte

func main() {
    fmt.Println(string(b))
}

在这个例子中,hello.txt 文件的内容被读取为字节切片,并可以通过 string 转换为字符串进行打印。

1.3 嵌入文件到文件系统

embed.FS 类型允许嵌入整个文件系统,这样可以方便地处理多个文件和目录。例如:

package main

import (
    "embed"
    "fmt"
    "log"
    "net/http"
)

//go:embed static/*
var content embed.FS

func main() {
    mux := http.NewServeMux()
    mux.Handle("/", http.FileServer(http.FS(content)))
    err := http.ListenAndServe(":8080", mux)
    if err != nil {
        log.Fatal(err)
    }
}

在这个例子中,static 目录下的所有文件会被嵌入到 content 变量中,并通过 http.FileServer 提供 HTTP 服务。

go:embed 指令详细用法说明

在变量声明前加上 //go:embed 指令,可以指定要嵌入哪些文件。这些文件可以通过一个或多个路径模式来匹配。

这个指令必须紧挨着一个单一变量的声明行。指令和变量声明之间只能有空行或 // 注释。

变量的类型必须是字符串类型、字节切片类型,或 FS(或 FS 的别名)。

例如:

package server

import "embed"

// content 是我们静态网页服务器的内容。
//go:embed image/* template/*
//go:embed html/index.html
var content embed.FS

Go 的构建系统会识别这些指令,并将匹配的文件从文件系统嵌入到声明的变量中(在上面的例子中是 content 变量)。

//go:embed 指令可以接受多个用空格分隔的模式,以简化书写。如果有很多模式,也可以重复使用 //go:embed,避免行太长。

模式是相对于包含源文件的包目录来解释的,路径分隔符是正斜杠(即使在 Windows 系统上也是如此)。模式中不能包含 ... 或空的路径元素,也不能以斜杠开头或结尾。要匹配当前目录中的所有内容,使用 * 代替 .

为了允许文件名中包含空格,模式可以写成 Go 的双引号或反引号字符串字面量。例如,//go:embed "my file.txt"

如果模式命名的是一个目录,所有在该目录下的文件都会递归地被嵌入,但文件名以 ._ 开头的文件会被排除。因此,上面的例子几乎等价于:

// content 是我们静态网页服务器的内容。
//go:embed image template html/index.html
var content embed.FS

不同的是,image/* 会嵌入 image/.tempfile 文件,而 image 则不会。两者都不会嵌入 image/dir/.tempfile

如果模式以 all: 前缀开头,那么遍历目录时也会包含以 ._ 开头的文件。例如,all:image 会嵌入 image/.tempfileimage/dir/.tempfile

//go:embed 指令可以用于导出的或未导出的变量,它只能用于包作用域的变量,不能用于局部变量。

模式不能匹配包模块之外的文件,比如含有.gitgo.mod 的目录或符号链接。模式也不能匹配文件名中包含特殊标点符号 " * < > ? ’ | / \ 和 : 的文件,**注意中文的问号也是非法名称**。匹配到的空目录会被忽略。此外,//go:embed` 行中的每个模式都必须至少匹配到一个文件或非空目录。

如果任何模式无效或有无效的匹配,编译将失败。

字符串和字节

对于类型为 string[]byte 的变量,//go:embed 行只能有一个模式,并且该模式只能匹配一个文件。字符串或 []byte 将初始化为该文件的内容。

即使使用 string[]byte 类型,//go:embed 指令也要求导入 embed 包。在不引用 embed.FS 的源文件中,使用空导入(import _ "embed")。

embed.FS 文件系统的读取操作

对于嵌入单个文件,通常使用 string[]byte 类型的变量最好。FS 类型则用于嵌入一棵文件树,比如一个静态网页服务器内容的目录,就像上面的例子一样。

FS 实现了 io/fs 包的 FS 接口,所以可以用于任何理解文件系统的包,包括 net/httptext/templatehtml/template。主要用于嵌入文件到 Go 程序中,并且这些文件在编译时会被打包到最终的二进制文件中。FS 是只读的,可以在多个 goroutine 中同时安全地使用。

FS 提供了以下几个方法,用于读取和操作嵌入的文件:

(1) Open(name string) (fs.File, error)

Open 方法用于打开指定名称的文件,并返回一个 fs.File 接口。

import "embed"

//go:embed static/*
var content embed.FS

file, err := content.Open("static/example.txt")
if err != nil {
	log.Fatal(err)
}
// 这里可以使用 file 进行文件操作,例如读取内容

返回的 fs.File 接口可以用于读取文件内容,如果打开的不是一个目录,它还实现了 io.Seekerio.ReaderAt 接口。

(2) ReadDir(name string) ([]fs.DirEntry, error)

ReadDir 方法用于读取指定目录的内容,并返回该目录中的所有条目(文件或子目录)。

entries, err := content.ReadDir("static")
if err != nil {
	log.Fatal(err)
}
for _, entry := range entries {
	fmt.Println(entry.Name(), entry.IsDir())
}

通过遍历 entries,可以获取到目录中的每个文件或子目录的信息。

(3) ReadFile(name string) ([]byte, error)

ReadFile 方法直接读取并返回指定文件的内容,以 []byte 类型返回。

data, err := content.ReadFile("static/example.txt")
if err != nil {
	log.Fatal(err)
}
fmt.Println(string(data))

这个方法适合用来读取较小的文件内容,返回值为文件的全部字节数据。

使用 FS 与其他包集成

由于 FS 实现了 fs.FS 接口,可以与许多标准库包进行集成。例如,使用 net/http 来提供嵌入文件的静态服务器:

http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(content))))
http.ListenAndServe(":8080", nil)

或者,使用 text/templatehtml/template 来解析嵌入的模板文件:

tmpl, err := template.ParseFS(content, "templates/*.tmpl")
if err != nil {
	log.Fatal(err)
}
tmpl.ExecuteTemplate(os.Stdout, "example.tmpl", nil)

如何查看 go:embeded 了哪些文件?

为了支持分析 Go 包的工具,Go 语言的工具链允许在 go list 命令的输出中显示 //go:embed 行中的模式。这意味着你可以通过 go list 命令查看项目中使用 //go:embed 指令的文件模式。这对分析工具和构建系统非常有用,特别是在自动化测试、文档生成、或其他需要了解嵌入文件内容的场景中。

假设你有一个包含以下内容的 Go 文件:

//go:embed hello.txt
var helloFile string

如果你运行 go list -json,你可能会看到类似以下的信息:

{
  "Dir": "/path/to/your/package",
  "GoFiles": ["main.go"],
  "EmbedPatterns": ["hello.txt"]
}

在这个示例中,EmbedPatterns 显示了 hello.txt 文件被嵌入到程序中。这种方式使得分析工具能够识别哪些文件被嵌入,以及它们的模式是什么,从而为进一步的自动化处理提供帮助。

通过这种方式,你也可以确认你的目录下的文件是否都已经被正确的嵌入,是否有存在希望被嵌入但因为非法名称而被忽略的文件。

4. 总结

embed 包为 Go 语言提供了强大的文件嵌入功能,使得将静态资源打包到程序中变得非常简单。无论是嵌入单个文件、多个文件还是整个目录,都可以通过简单的配置和指令完成。这种方式不仅简化了资源管理,还使得部署更加方便。希望本文对你理解和使用 embed 包有所帮助!


也可以看看