go:embed:把文件打包进二进制
你有没有遇到过这样的烦恼?
编译好的 Go 程序发给别人,结果对方说:“怎么打开是 404 啊?” 你一看,原来是因为 HTML 模板、CSS 文件、图片这些静态资源没有一起打包过去。
Go 1.16 引入的 //go:embed 指令彻底解决了这个问题。它允许你把任意文件直接嵌入到 Go 的二进制文件中,让你的程序变成一个真正的"单文件"应用。
基本用法
嵌入单个文件
package main
import (
_ "embed"
"fmt"
)
//go:embed hello.txt
var hello string
func main() {
fmt.Println(hello)
}
就这么简单!//go:embed 指令告诉编译器:在编译时,把 hello.txt 的内容嵌入到变量 hello 中。
注意几个细节:
//go:embed和变量声明之间不能有空行- 导入
_ "embed"是必须的(即使你没有直接使用这个包) - 变量类型可以是
string、[]byte或embed.FS
嵌入为字节切片
package main
import (
_ "embed"
"fmt"
)
//go:embed logo.png
var logo []byte
func main() {
fmt.Printf("Logo 大小: %d bytes\n", len(logo))
}
嵌入多个文件(embed.FS)
当你需要嵌入多个文件时,使用 embed.FS 类型:
package main
import (
"embed"
"fmt"
"io/fs"
)
//go:embed static/*
var staticFiles embed.FS
//go:embed templates/*
var templateFiles embed.FS
func main() {
// 遍历嵌入的文件
fs.WalkDir(staticFiles, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Println(path)
return nil
})
// 读取单个文件
data, err := staticFiles.ReadFile("static/style.css")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("CSS 内容: %s\n", data)
}
嵌入模式匹配
//go:embed 支持 glob 模式匹配:
// 嵌入所有 .txt 文件
//go:embed *.txt
var textFiles embed.FS
// 嵌入子目录中的所有文件
//go:embed images/*
var images embed.FS
// 嵌入多个模式
//go:embed *.html *.css *.js
var webFiles embed.FS
// 嵌入当前目录和所有子目录
//go:embed all:assets
var allAssets embed.FS
all: 前缀
默认情况下,//go:embed 会忽略以 _ 或 . 开头的文件。如果你需要包含这些文件,使用 all: 前缀:
//go:embed all:static
var staticFiles embed.FS
// 这会包含 .hidden 文件和 _temp 文件
实战:构建单文件 Web 服务器
让我们用 go:embed 构建一个真正的单文件 Web 服务器:
package main
import (
"embed"
"html/template"
"io/fs"
"log"
"net/http"
)
//go:embed templates/*
var templateFS embed.FS
//go:embed static/*
var staticFS embed.FS
var templates *template.Template
func init() {
// 从嵌入的文件系统解析模板
templates = template.Must(
template.ParseFS(templateFS, "templates/*.html"),
)
}
func main() {
// 提供静态文件
staticSub, _ := fs.Sub(staticFS, "static")
http.Handle("/static/", http.StripPrefix("/static/",
http.FileServer(http.FS(staticSub))))
// 路由
http.HandleFunc("/", homeHandler)
http.HandleFunc("/about", aboutHandler)
http.HandleFunc("/api/health", healthHandler)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"Title": "首页",
"Message": "欢迎来到我的网站!",
}
templates.ExecuteTemplate(w, "home.html", data)
}
func aboutHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"Title": "关于我们",
}
templates.ExecuteTemplate(w, "about.html", data)
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
}
项目结构:
myapp/
├── main.go
├── templates/
│ ├── home.html
│ └── about.html
└── static/
├── style.css
├── app.js
└── logo.png
编译后,你会得到一个单一的可执行文件,它包含了所有的 HTML、CSS、JavaScript 和图片文件。把这个文件发给别人,直接运行就能启动一个完整的 Web 服务器!
嵌入配置文件
一个很实用的场景是把默认配置嵌入到程序中:
package main
import (
_ "embed"
"fmt"
"os"
"gopkg.in/yaml.v3"
)
//go:embed default_config.yaml
var defaultConfig []byte
type Config struct {
Server struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
} `yaml:"server"`
Database struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Name string `yaml:"name"`
User string `yaml:"user"`
Password string `yaml:"password"`
} `yaml:"database"`
}
func loadConfig() (*Config, error) {
config := &Config{}
// 先加载默认配置
if err := yaml.Unmarshal(defaultConfig, config); err != nil {
return nil, fmt.Errorf("parse default config: %w", err)
}
// 尝试加载用户配置文件
if _, err := os.Stat("config.yaml"); err == nil {
data, err := os.ReadFile("config.yaml")
if err != nil {
return nil, fmt.Errorf("read config file: %w", err)
}
if err := yaml.Unmarshal(data, config); err != nil {
return nil, fmt.Errorf("parse config file: %w", err)
}
}
return config, nil
}
func main() {
config, err := loadConfig()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Server: %s:%d\n", config.Server.Host, config.Server.Port)
fmt.Printf("Database: %s:%d/%s\n",
config.Database.Host,
config.Database.Port,
config.Database.Name)
}
这样,即使用户没有提供配置文件,程序也能使用嵌入的默认配置正常运行。
嵌入 SQL 迁移文件
数据库迁移文件也可以嵌入:
package main
import (
"database/sql"
"embed"
"fmt"
"io/fs"
"sort"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
func runMigrations(db *sql.DB) error {
// 读取所有迁移文件
entries, err := fs.ReadDir(migrationsFS, "migrations")
if err != nil {
return err
}
// 按文件名排序(确保顺序执行)
sort.Slice(entries, func(i, j int) bool {
return entries[i].Name() < entries[j].Name()
})
for _, entry := range entries {
if entry.IsDir() {
continue
}
data, err := fs.ReadFile(migrationsFS, "migrations/"+entry.Name())
if err != nil {
return fmt.Errorf("read %s: %w", entry.Name(), err)
}
fmt.Printf("Running migration: %s\n", entry.Name())
if _, err := db.Exec(string(data)); err != nil {
return fmt.Errorf("execute %s: %w", entry.Name(), err)
}
}
return nil
}
注意事项
文件路径
嵌入的文件路径是相对于 Go 源文件的:
project/
├── main.go # //go:embed 在这里
├── config.yaml # ✅ 可以嵌入
└── pkg/
├── server.go # //go:embed 在这里
└── data/
└── file.txt # ✅ server.go 可以嵌入这个文件
不能嵌入的文件
- 不能嵌入
..路径(父目录) - 不能嵌入符号链接指向的文件
- 不能嵌入 Go 模块之外的文件
编译时 vs 运行时
记住://go:embed 是在编译时执行的。这意味着:
//go:embed version.txt
var version string
如果你修改了 version.txt,必须重新编译程序才能看到变化。这对于部署来说是好事(不需要带额外文件),但对于开发来说可能不太方便。
结合 HTTP 服务器的高级用法
package main
import (
"embed"
"io/fs"
"log"
"net/http"
"os"
)
//go:embed dist/*
var embeddedFS embed.FS
func main() {
var fileSystem http.FileSystem
// 开发模式:使用磁盘上的文件
if os.Getenv("DEV") == "1" {
fileSystem = http.Dir("dist")
log.Println("Using filesystem (dev mode)")
} else {
// 生产模式:使用嵌入的文件
sub, _ := fs.Sub(embeddedFS, "dist")
fileSystem = http.FS(sub)
log.Println("Using embedded files (prod mode)")
}
http.Handle("/", http.FileServer(fileSystem))
log.Fatal(http.ListenAndServe(":8080", nil))
}
这样你可以在开发时使用 DEV=1 go run main.go,修改文件后立即看到效果;生产环境直接编译,所有资源都嵌入到二进制中。
总结
go:embed 是 Go 1.16 引入的一个非常实用的特性。它让你的程序变成了一个真正的"单文件"应用,大大简化了部署流程。
主要用途:
- 嵌入 HTML 模板、CSS、JavaScript 等 Web 资源
- 嵌入默认配置文件
- 嵌入数据库迁移文件
- 嵌入图片、字体等二进制资源
- 嵌入版本信息、许可证等元数据
下次当你需要分发一个 Go 程序时,考虑使用 go:embed 把所有资源打包进去,让用户只需要一个文件就能运行你的应用。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。