go:embed:把文件打包进二进制

学习 Go 1.16 引入的 embed 包,将静态资源直接嵌入到可执行文件中

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[]byteembed.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 把所有资源打包进去,让用户只需要一个文件就能运行你的应用。

继续阅读

探索更多技术文章

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

全部文章 返回首页