bigywater.tech
Go · 41

go:embed:文件是怎么被打进二进制并通过 fs.FS 读出来的

go:embed 让二进制自带静态资源,不用再带一个 assets 目录部署。这篇逐项验证它的规则:怎么写、哪些文件被嵌进去、embed.FS 里的文件长什么样、配合 net/http 的表现、读取成本,以及数据在二进制里的位置与体积,最后列出常见的编译错误。

3 / 7 / 5
同一个 assets 目录,三种写法嵌入的文件数:默认 3 个,all: 前缀 7 个,通配符 5 个
2.02 MiB
嵌入 1 MiB 文件,同时声明 []byte 与 string 时二进制增加的大小
57.48 ns
embed.FS.ReadFile 的耗时(2 次分配);string 变量是 1.92 ns

三种变量与目录规则

实测 · 本机执行输出
  string 变量:"hello, embed";[]byte 变量:"hello, embed"(len 12);embed.FS 单文件:"hello, embed"
  //go:embed assets                  3 个文件:assets/a.txt assets/b.txt assets/sub/c.md
  //go:embed all:assets              7 个文件:assets/.hidden assets/_under.txt assets/a.txt assets/b.txt assets/sub/.secret assets/sub/_x.md assets/sub/c.md
  //go:embed assets/*.txt assets/sub/*.md 5 个文件:assets/_under.txt assets/a.txt assets/b.txt assets/sub/_x.md assets/sub/c.md

单个文件可以嵌到 string、[]byte 或 embed.FS;目录或多个文件只能嵌到 embed.FS。目录模式默认排除以 . 或 _ 开头的文件(防止把 .env 一类文件打进去),要包含就加 all: 前缀;通配符匹配到的文件不受这条限制。

embed.FS 里的文件

实测 · 本机执行输出
  ReadDir("assets") 的顺序:a.txt b.txt sub(sort.StringsAreSorted = true)
  Stat(assets/a.txt):Size = 7,Mode = -r--r--r--,ModTime.IsZero() = true
  读不存在的文件:open assets/不存在.txt: file does not exist
  路径带前导斜杠:open /assets/a.txt: file does not exist
  fs.Sub(dirFS, "assets") 后读 sub/c.md:"# c\n"
  修改 []byte 变量(helloBytes[0] = 'J'):"Jello, embed";再从 embed.FS 读:"hello, embed"

它实现了 fs.FS(并有 ReadFile、ReadDir),文件只读(-r--r--r--)、按名字排序、修改时间为零值。路径用不带前导斜杠的 / 分隔形式。[]byte 变量是数据段里的副本,可以修改,但不影响 embed.FS 里的内容。

配合 http.FS

实测 · 本机执行输出
  GET /a.txt     → 200,Content-Type "text/plain; charset=utf-8",Last-Modified "",Content-Length "7",正文前 20 字节 "file a\n"
  GET /sub/c.md  → 200,Content-Type "text/plain; charset=utf-8",Last-Modified "",Content-Length "4",正文前 20 字节 "# c\n"
  GET /missing   → 404,Content-Type "text/plain; charset=utf-8",Last-Modified "",Content-Length "",正文前 20 字节 "404 page not found\n"
  GET /          → 200,Content-Type "text/html; charset=utf-8",Last-Modified "",Content-Length "",正文前 20 字节 "<!doctype html>\n<met"

http.FS 把 fs.FS 变成 http.FileSystem,静态文件服务一行搞定。因为修改时间是零值,响应没有 Last-Modified,浏览器就不会做基于时间的条件请求;要缓存控制得自己加 Cache-Control/ETag(本文没有试)。

读取成本

实测 · 本机执行输出
  使用 string 变量(无读取)                    中位 1.95 ns/op(最小 1.91 / 最大 2.21)
  embed.FS.ReadFile("hello.txt")       中位 54.85 ns/op(最小 54.72 / 最大 55.38)
  fs.ReadFile(dirFS, "assets/a.txt")   中位 84.56 ns/op(最小 83.87 / 最大 86.50)
  dirFS.Open + Stat + Close            中位 70.31 ns/op(最小 68.69 / 最大 84.23)
  AllocsPerRun:ReadFile 2 次;使用 string 变量 0 次

ReadFile 要把内容拷贝到新分配的切片里(2 次分配),约 57 ns;直接用 string 变量没有任何运行时成本。内容很小但读得很频繁时,优先嵌成 string。

在二进制里

实测 · 本机执行输出
  不嵌入的同类程序 2429698 字节;嵌入 1 MiB(1048576 字节)文件,同时用 []byte 与 string 各声明一次:4543330 字节,增加 2113632 字节(约 2.02 MiB)
  go tool nm -size 里 ≥ 1 MiB 的符号:
    <地址>    1085352 R runtime.rodata
    <地址>    1085352 R go:string.*
    <地址>    1048576 D main..gobytes.1
  同一个文件用 []byte 与 string 各声明一次,体积共多 2.02 MiB:[]byte 的数据在可写的数据段(符号类型 D),string 在只读段(R),各占一份,没有合并

嵌入的内容就是二进制的一部分:体积直接增加文件大小。实验里同一个文件声明了 []byte 与 string 两次,符号表里就有两份 1 MiB 的数据(只读段的 go:string.* 与数据段的 main..gobytes.1),所以不要为同一个文件重复声明。

编译错误

实测 · 本机执行输出
  匹配不到文件:
    main.go:7:12: pattern nothing.txt: no matching files found
  写到模块之外(../):
    main.go:7:12: pattern ../a.txt: invalid pattern syntax
  在函数里声明:
    main.go:8:4: go:embed cannot apply to var inside func
  变量类型是 int:
    main.go:8:5: go:embed cannot apply to var of type int
  string 变量没有导入 embed 包:
    main.go:3:3: go:embed requires import "embed" (or import _ "embed", if package is not used)
  把含两个文件的目录嵌进 string:
    main.go:8:5: invalid go:embed: multiple files for type string
  把只含 1 个文件(内容 "xx")的目录嵌进 string:编译 rc=0;运行输出 len, s = '2 xx'

写错的信息都很直接。有一个容易忽略的特例:把一个只含一个文件的目录嵌进 string 是允许的(得到那个文件的内容),文件变成两个就报错,所以对目录用 embed.FS 更不容易出意外。

分步演示

go:embed
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。所有输出来自真实运行;http 部分用 httptest 直接调用 handler;体积数字来自 go build 的默认选项;符号来自 go tool nm -size
    • 没有验证:嵌入符号链接与其它特殊文件的规则、go:embed 对构建缓存与增量编译的影响、与 //go:generate 配合生成资源的流程、大文件(几百 MiB)对链接时间与内存的影响、压缩后嵌入(如 gzip)的做法
    京ICP备2026067502号-1京公网安备11010502063487号