Go embed 入门:把模板和静态文件打进二进制

本文讲解 Go embed 的基本用法,包括嵌入单个文件、目录、HTML 模板和静态资源,让小型 Web 工具更容易发布。

为什么要把资源打进二进制

Go 程序发布时常常是一个二进制文件,这很方便。但只要你的程序依赖模板、CSS、默认配置、SQL 迁移脚本或静态图片,发布就不再只是复制一个文件。你需要保证资源目录也在正确位置,工作目录也正确。很多“小工具在我机器上能跑,复制到服务器就找不到模板”的问题都来自这里。

embed 可以把文件内容在编译时嵌入 Go 二进制。这样模板和静态资源跟着程序走,部署更简单。它不是所有场景的答案,用户上传文件、运行时配置、频繁变化的大资源都不适合 embed。但对小型 Web 工具、内部管理页面、默认模板和初始化脚本,它非常实用。

这篇文章讲单文件、目录、模板和静态文件服务四种用法。

嵌入单个文件

目录:

app/
├── main.go
└── VERSION.txt

VERSION.txt

v1.0.0

代码:

package main

import (
	_ "embed"
	"fmt"
	"strings"
)

//go:embed VERSION.txt
var versionText string

func main() {
	fmt.Println(strings.TrimSpace(versionText))
}

注意必须导入 embed,即使只用空白导入。//go:embed 注释必须紧挨着变量声明,中间不能有空行。变量可以是 string[]byteembed.FS

嵌入模板目录

目录:

templates/
├── layout.html
└── index.html

代码:

import (
	"embed"
	"html/template"
)

//go:embed templates/*.html
var templateFS embed.FS

func parseTemplates() (*template.Template, error) {
	return template.ParseFS(templateFS, "templates/*.html")
}

Handler:

func indexHandler(tmpl *template.Template) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		data := struct {
			Title string
		}{
			Title: "Go embed 入门",
		}

		if err := tmpl.ExecuteTemplate(w, "index.html", data); err != nil {
			http.Error(w, "render error", http.StatusInternalServerError)
		}
	}
}

模板在编译时打进二进制,运行时不再依赖当前工作目录。这个变化对部署很友好。

嵌入静态资源

目录:

static/
├── css/app.css
└── images/logo.png

代码:

//go:embed static
var staticFS embed.FS

提供 HTTP 文件服务:

func staticHandler() http.Handler {
	fs := http.FS(staticFS)
	return http.FileServer(fs)
}

注册:

mux.Handle("/static/", staticHandler())

这里有一个细节:embed.FS 里路径仍然包含 static/ 前缀。访问 /static/css/app.css 时,文件系统里也有 static/css/app.css,所以可以直接 FileServer。如果你想去掉前缀,可以使用 fs.Sub

sub, err := fs.Sub(staticFS, "static")
if err != nil {
	return err
}
mux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(sub))))

这样访问 /assets/css/app.css 会读取嵌入目录里的 css/app.css

什么时候不适合 embed

不适合嵌入的内容:

  • 用户上传文件
  • 运行时修改的配置
  • 很大的视频或数据包
  • 希望无需重新编译就更新的模板
  • 密钥和敏感配置

embed 是编译时能力。文件变了,必须重新构建程序。它也会增加二进制体积。小型模板、CSS、默认配置很适合;大体积资源要谨慎。

也不要把密钥放进 embed。二进制可以被复制和分析,嵌入密钥并不安全。密钥仍然应该通过环境变量、密钥管理系统或安全配置注入。

测试嵌入文件

可以测试模板是否能解析:

func TestParseTemplates(t *testing.T) {
	tmpl, err := parseTemplates()
	if err != nil {
		t.Fatalf("parse templates: %v", err)
	}
	if tmpl.Lookup("index.html") == nil {
		t.Fatal("index.html not found")
	}
}

这种测试能在资源路径写错时尽早失败。embed 路径错误通常是编译期或启动期问题,最好不要等到用户访问页面才发现。

部署时的一个真实好处

很多小工具最麻烦的不是代码,而是部署说明。你写了一个后台页面,本地运行没问题,放到服务器后却发现模板目录没带上,或者工作目录不同导致 open templates/index.html: no such file or directoryembed 可以把这类问题提前到编译阶段。只要二进制能启动,模板和默认静态资源就在里面。

当然,这不表示所有项目都应该嵌入前端资源。大型前端通常有自己的构建、缓存和 CDN 策略,嵌进 Go 二进制反而会让发布粒度变粗。更适合 embed 的场景是内部管理页、命令行工具的默认模板、邮件模板、迁移脚本、示例配置和少量帮助文档。判断标准很简单:资源是否小、是否稳定、是否应该随程序版本一起发布。

还要注意开发体验。模板频繁修改时,每次都重新编译会有点慢。可以在开发模式下从磁盘读取,生产模式再使用嵌入文件系统:

func templateFS(dev bool) fs.FS {
	if dev {
		return os.DirFS("templates")
	}
	sub, err := fs.Sub(embeddedFiles, "templates")
	if err != nil {
		panic(err)
	}
	return sub
}

这样既保留了生产部署的稳定性,也不会让本地调试变得笨重。

还有一个容易忽略的细节:嵌入文件的路径是相对当前源码文件所在目录匹配的,不是相对运行程序时的工作目录。把资源目录移动位置后,要同步调整 //go:embed 的模式。建议资源目录和使用它的 Go 文件放得近一点,比如 internal/web/templatesinternal/web/assets,这样代码审查时更容易看出二者关系。目录层级过深时,路径问题会变成维护成本。

如果项目有多套模板,也建议按功能拆目录,而不是把所有文件平铺到一个大目录里。

小结

embed 让 Go 程序可以把模板、静态文件和默认资源打进二进制,减少部署时的路径问题。单文件可以嵌入到 string[]byte,目录可以嵌入到 embed.FS,模板用 template.ParseFS,静态资源可以配合 http.FSfs.Sub

它适合小而稳定的资源,不适合运行时变化、大体积数据和敏感密钥。理解这个边界后,embed 会让很多小型 Go Web 工具发布起来更省心。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页