go embed 编译加入静态文件

什么是 go embed

背景: go 编译,默认只会将 go 文件打包出一个可执行的二进制文件,但是实际使用中,可能我们还会用到一些配置环境变量的文件或者是静态资源文件,而 go embed 的功能就是将这些静态文件打包进入二进制文件中

功能

go embed 的功能1

  1. 对于单个的文件,支持嵌入为字符串和 byte slice
  2. 对于多个文件和文件夹,支持嵌入为新的文件系统 FS
  3. 比如导入 “embed"包,即使无显式的使用
  4. go:embed 指令用来嵌入,必须紧跟着嵌入后的变量名
  5. 只支持嵌入为 string, byte slice 和 embed. FS 三种类型,这三种类型的别名 (alias) 和命名类型 (如 type S string) 都不可以

关于第三点的解释 “比如导入 ’embed’包,即使无显式的使用” 是指在 Go 语言中,在代码中导入一个包(如 import "embed")但没有直接使用这个包中的任何函数、类型或变量时的情况。

在 Go 语言中,通常情况下,如果我们导入了一个包但没有使用它,编译器会报错。这是 Go 的一个特性,用来避免不必要的依赖。

然而,有些包(如 embed 包)是用于特殊目的的。embed 包是 Go 1.16 引入的,它允许在编译时将文件嵌入到 Go 程序中。即使我们没有显式地调用 embed 包中的函数,仅仅通过导入它,并使用特殊的注释(如 //go:embed)就能触发其功能。

例如:

1
2
3
4
import _ "embed"  // 使用下划线导入,表示我们知道这个包没有被直接使用

//go:embed hello.txt
var s string  // 这里使用了embed包的功能,但没有直接调用embed包中的任何函数

这种情况下,虽然看起来没有显式使用 embed 包,但实际上通过特殊注释触发了它的功能,所以需要导入。

关于第二点,当多个文件是可以通过一个 embed. FS 定义, 然后多次读入这个变量,获取每个文件的内容

相对路径解析

当使用 Go 的 embed 包嵌入文件时,相对路径的根路径 (基准目录) 是包含当前 Go 源文件的目录,而不是工作目录或项目根目录。这一点非常重要,因为它决定了嵌入文件的查找方式。

以下是一个例子展示文件结构和相对路径的使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
项目结构:
/myproject
  /cmd
    main.go
  /internal
    /app
      app.go
      /templates
        home.html
  /static
    logo.png

app.go 中嵌入文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package app

import "embed"

// 嵌入相同目录中的templates文件夹下的文件
//go:embed templates/home.html
var homeTemplate []byte

// 嵌入上级目录的文件
//go:embed ../../static/logo.png
var logo []byte

处理特殊字符文件名

对于包含空格、特殊字符或非 ASCII 字符的文件或目录名,Go 的 embed 包允许使用双引号 " 或反引号 ` 来包围文件路径或模式。这在处理实际项目中可能遇到的复杂文件名时非常有用。

使用双引号

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "embed"

// 使用双引号处理包含空格的文件名
//go:embed "files/my document.pdf"
var document []byte

// 使用双引号处理包含特殊字符的文件名
//go:embed "data/config#1.json"
var config []byte

使用反引号 (推荐)

反引号更适合处理包含特殊字符的路径,因为它们不需要转义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "embed"

// 使用反引号处理包含空格和特殊字符的文件名
//go:embed `resources/user data/profile-2023.json`
var userData []byte

// 对于包含Windows路径分隔符的情况
//go:embed `configs\special\config.yaml`
var winConfig []byte

模式匹配中使用引号

在使用通配符模式时也可以使用引号:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "embed"

// 嵌入所有带空格名称的JSON文件
//go:embed `data/customer reports/*.json`
var reports embed.FS

// 嵌入特定目录中的所有文件
//go:embed "static/images (2022)/*"
var images embed.FS

读取文件的方式, 传统方式与新方式对比

传统方式:使用本地文件系统

在 Go 的 net/http 包中,传统上我们通过指向物理路径的方式提供静态文件服务:

1
2
// 传统方式:直接使用物理文件系统
http.Handle("/", http.FileServer(http.Dir("/tmp")))

这种方式直接从服务器的文件系统读取文件,将 /tmp 目录下的内容暴露给 HTTP 请求者。

新方式:使用嵌入文件系统

Go 1.16 引入的 embed 包和 io/fs 接口使得可以将静态文件嵌入到二进制文件中,并通过 HTTP 提供服务:

1
2
// 新方式:使用嵌入的文件系统
http.Handle("/", http.FileServer(http.FS(fsys)))

这里的关键变化是 http.FS() 函数,它是一个适配器,将任何实现了 fs.FS 接口的文件系统包装成 http.FileSystem 接口,使其可以被 http.FileServer 使用。

三个个点

  • embed 实现了 fs. FS 的接口
  • http.FileSystem - HTTP 包使用的文件系统接口
  • http.FS() - 适配器函数,将 fs.FS 转换为 http.FileSystem

嵌入文件服务的优势

  1. 单一二进制文件部署 - 所有静态资源都嵌入到单个可执行文件中,无需额外的文件传输或管理

  2. 跨平台一致性 - 不依赖于运行环境的文件系统结构,确保在不同平台上行为一致

  3. 安全性 - 嵌入文件内容不能被外部修改,防止文件篡改

  4. 性能 - 嵌入文件加载速度快,不需要从磁盘读取

  5. 简化部署 - 减少了部署时需要管理的文件,特别是在云环境或容器中部署时非常有价值

小总结

针对场景

  1. 字符
  2. 单文件 embed. FS
  3. 多文件 embed. FS
  4. 支持定义 exported 或者 unexported 变量
  5. 编译之初就将值嵌入,后续不变,读取同一个文件初始化两个变量,两个变量属于不同地址的字段
  6. 线程安全,FS 文件系统只提供打开和读取的方法,多个 goroutine 可以并发使用
  7. 支持直接读取文件夹
  8. 读取特殊文件名的文件,使用反引号作为转义字符
  9. 使用 embed 时默认根路径在使用 embed 字符的文件的路径
  10. embed. FS 实现了一个 io/fs. FS 接口

参考

Footnotes