go:embed 让二进制自带静态资源,不用再带一个 assets 目录部署。这篇逐项验证它的规则:怎么写、哪些文件被嵌进去、embed.FS 里的文件长什么样、配合 net/http 的表现、读取成本,以及数据在二进制里的位置与体积,最后列出常见的编译错误。
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: 前缀;通配符匹配到的文件不受这条限制。
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 里的内容。
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 更不容易出意外。