Как встраивать статические ресурсы в бинарные файлы Go

Хотите узнать, как использовать HTML-шаблоны, CSS, JavaScript, картинки и конфиги прямо внутри вашего Go-приложения? Давайте разберёмся, как это сделать с помощью пакета embed.

Одна из самых крутых фишек Go — это возможность собрать всё приложение в один-единственный бинарный файл. И начиная с версии 1.16, пакет embed делает процесс встраивания статических файлов простым и интуитивным. Забудьте о ситуациях, когда в продакшене вдруг не хватает какого-то файла или ресурса: теперь всё, что нужно, будет всегда под рукой — прямо внутри исполняемого файла.

Пакет embed

Пакет embed в Go позволяет включать файлы на этапе компиляции:

package main

import (
    "embed"
    "fmt"
)

//go:embed hello.txt
var content string

func main() {
    fmt.Println(content)
}

Директива //go:embed указывает компилятору включить файл hello.txt в бинарный файл.

Шаблоны встраивания

Один файл как строка

package main

import (
    _ "embed"
    "fmt"
)

//go:embed config.json
var configJSON string

func main() {
    fmt.Println(configJSON)
}

Один файл как байты

package main

import (
    _ "embed"
)

//go:embed image.png
var imageData []byte

func main() {
    // imageData содержит сырые байты PNG-файла
    fmt.Printf("Размер изображения: %d байт\n", len(imageData))
}

Каталог как embed.FS

package main

import (
    "embed"
    "io/fs"
    "log"
)

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

func main() {
    // Список всех файлов в templates/
    entries, err := fs.ReadDir(templates, "templates")
    if err != nil {
        log.Fatal(err)
    }

    for _, entry := range entries {
        fmt.Println(entry.Name())
    }
}

Типичные сценарии использования

Встраивание HTML-шаблонов

package main

import (
    "embed"
    "html/template"
    "net/http"
)

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

var templates *template.Template

func init() {
    // Парсинг всех шаблонов из встроенной файловой системы
    templates = template.Must(template.ParseFS(templateFS, "templates/*.html"))
}

type PageData struct {
    Title   string
    Message string
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        data := PageData{
            Title:   "Welcome",
            Message: "Hello from embedded templates!",
        }
        templates.ExecuteTemplate(w, "index.html", data)
    })

    http.ListenAndServe(":8080", nil)
}

Создайте файл templates/index.html:

<!DOCTYPE html>
<html>
<head>
    <title>{{.Title}}</title>
</head>
<body>
    <h1>{{.Message}}</h1>
</body>
</html>

Встраивание статических веб-ресурсов

package main

import (
    "embed"
    "io/fs"
    "net/http"
)

//go:embed static/*
var staticFS embed.FS

func main() {
    // Убираем префикс "static" из путей
    staticContent, _ := fs.Sub(staticFS, "static")

    // Обслуживание файлов по адресу /static/
    http.Handle("/static/", http.StripPrefix("/static/", 
        http.FileServer(http.FS(staticContent))))

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "text/html")
        w.Write([]byte(`
            <!DOCTYPE html>
            <html>
            <head>
                <link rel="stylesheet" href="/static/style.css">
            </head>
            <body>
                <h1>Hello!</h1>
                <script src="/static/app.js"></script>
            </body>
            </html>
        `))
    })

    http.ListenAndServe(":8080", nil)
}

Встраивание конфигурационных файлов

package main

import (
    _ "embed"
    "encoding/json"
    "fmt"
)

//go:embed config/defaults.json
var defaultConfigJSON []byte

type Config struct {
    AppName     string   `json:"appName"`
    Port        int      `json:"port"`
    Debug       bool     `json:"debug"`
    AllowedHosts []string `json:"allowedHosts"`
}

func loadConfig() (*Config, error) {
    var config Config
    if err := json.Unmarshal(defaultConfigJSON, &config); err != nil {
        return nil, err
    }
    return &config, nil
}

func main() {
    config, err := loadConfig()
    if err != nil {
        panic(err)
    }

    fmt.Printf("Приложение: %s, Порт: %d\n", config.AppName, config.Port)
}

Встраивание файлов миграций SQL

package main

import (
    "database/sql"
    "embed"
    "fmt"
    "io/fs"
    "sort"
    "strings"
)

//go:embed migrations/*.sql
var migrationsFS embed.FS

func runMigrations(db *sql.DB) error {
    // Получение всех SQL-файлов
    entries, err := fs.ReadDir(migrationsFS, "migrations")
    if err != nil {
        return err
    }

    // Сортировка миграций по имени файла (например, 001_create_users.sql)
    var filenames []string
    for _, entry := range entries {
        if strings.HasSuffix(entry.Name(), ".sql") {
            filenames = append(filenames, entry.Name())
        }
    }
    sort.Strings(filenames)

    // Выполнение каждой миграции
    for _, filename := range filenames {
        content, err := fs.ReadFile(migrationsFS, "migrations/"+filename)
        if err != nil {
            return err
        }

        fmt.Printf("Выполнение миграции: %s\n", filename)
        if _, err := db.Exec(string(content)); err != nil {
            return fmt.Errorf("миграция %s не выполнена: %w", filename, err)
        }
    }

    return nil
}

func main() {
    // Пример использования (требуется реальное подключение к БД)
    fmt.Println("Миграции успешно встроены")

    // Список встроенных миграций
    entries, _ := fs.ReadDir(migrationsFS, "migrations")
    for _, e := range entries {
        fmt.Println(" -", e.Name())
    }
}

Несколько директив embed

Вы можете встраивать несколько шаблонов:

package main

import (
    "embed"
)

// Встраивание нескольких каталогов
//go:embed templates/* static/* config/*
var assets embed.FS

// Или использование нескольких директив
//go:embed templates/*
//go:embed static/*
//go:embed config/*
var multiAssets embed.FS

Рекурсивное встраивание

Используйте префикс all: для включения файлов, начинающихся с . или _:

package main

import (
    "embed"
)

// Обычное встраивание — исключает .gitkeep, _hidden и т.д.
//go:embed static/*
var publicAssets embed.FS

// С префиксом all: — включает ВСЕ файлы
//go:embed all:static/*
var allAssets embed.FS

Сопоставление с образцом (Pattern Matching)

Embed поддерживает glob-шаблоны:

package main

import (
    "embed"
)

// Все .go файлы в текущем каталоге
//go:embed *.go
var sourceFiles embed.FS

// Все JSON-файлы рекурсивно
//go:embed config/**/*.json
var configFiles embed.FS

// Несколько конкретных файлов
//go:embed logo.png favicon.ico robots.txt
var rootFiles embed.FS

Шаблон для разработки и продакшена

Используйте теги сборки для переключения между встроенными и «живыми» файлами:

// файл: assets_prod.go
//go:build !dev

package main

import (
    "embed"
    "io/fs"
)

//go:embed static/*
var staticFS embed.FS

func getStaticFS() fs.FS {
    sub, _ := fs.Sub(staticFS, "static")
    return sub
}
// файл: assets_dev.go
//go:build dev

package main

import (
    "io/fs"
    "os"
)

func getStaticFS() fs.FS {
    // Использование реальной файловой системы во время разработки
    return os.DirFS("static")
}

Сборка для продакшена:

go build -o myapp

Сборка для разработки:

go build -tags dev -o myapp

Встраивание информации о версии

package main

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

//go:embed version.txt
var version string

func main() {
    fmt.Printf("Версия: %s\n", strings.TrimSpace(version))
}

Полный пример веб-сервера

package main

import (
    "embed"
    "encoding/json"
    "html/template"
    "io/fs"
    "log"
    "net/http"
)

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

//go:embed web/static/*
var staticFS embed.FS

//go:embed config/app.json
var configJSON []byte

type Config struct {
    Port    string `json:"port"`
    AppName string `json:"appName"`
}

type Server struct {
    config    *Config
    templates *template.Template
}

func NewServer() (*Server, error) {
    // Загрузка конфигурации
    var config Config
    if err := json.Unmarshal(configJSON, &config); err != nil {
        return nil, err
    }

    // Парсинг шаблонов
    templates, err := template.ParseFS(templateFS, "web/templates/*.html")
    if err != nil {
        return nil, err
    }

    return &Server{
        config:    &config,
        templates: templates,
    }, nil
}

func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
    data := map[string]string{
        "AppName": s.config.AppName,
    }
    s.templates.ExecuteTemplate(w, "home.html", data)
}

func main() {
    server, err := NewServer()
    if err != nil {
        log.Fatal(err)
    }

    // Обслуживание статических файлов
    staticContent, _ := fs.Sub(staticFS, "web/static")
    http.Handle("/static/", http.StripPrefix("/static/", 
        http.FileServer(http.FS(staticContent))))

    // Маршруты
    http.HandleFunc("/", server.handleHome)

    log.Printf("Запуск %s на порту %s", server.config.AppName, server.config.Port)
    log.Fatal(http.ListenAndServe(":"+server.config.Port, nil))
}

Проверка встроенных файлов во время выполнения

package main

import (
    "embed"
    "fmt"
    "io/fs"
)

//go:embed assets/*
var assets embed.FS

func listEmbeddedFiles() {
    fs.WalkDir(assets, ".", func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }

        if !d.IsDir() {
            info, _ := d.Info()
            fmt.Printf("%s (%d байт)\n", path, info.Size())
        }
        return nil
    })
}

func main() {
    fmt.Println("Встроенные файлы:")
    listEmbeddedFiles()
}

Ограничения и важные моменты

  • Только на этапе компиляции: изменения требуют повторной компиляции
  • Использование памяти: встроенные файлы хранятся в оперативной памяти
  • Отсутствие поддержки символьных ссылок: символические ссылки не обрабатываются
  • Разделители путей: всегда используйте прямые слеши /
  • Относительные пути: пути указываются относительно исходного файла Go
// НЕВЕРНО — абсолютные пути не работают
//go:embed /etc/config.json

// НЕВЕРНО — родительские каталоги не работают
//go:embed ../shared/config.json

// ВЕРНО — путь относительно текущего исходного файла
//go:embed config.json

Резюме

Пакет embed предоставляет несколько способов встраивания файлов:

Тип переменнойНазначение
stringОдин текстовый файл
[]byteОдин бинарный файл
embed.FSНесколько файлов или каталогов

Рекомендации по использованию:

  • Используйте embed.FS для группы связанных файлов
  • Применяйте fs.Sub() для создания чистых префиксов путей
  • Рассмотрите использование тегов сборки для режима разработки
  • Встраивайте конфигурационные значения по умолчанию, переопределяя их во время выполнения
  • Учитывайте размер бинарного файла при работе с крупными ресурсами

Готовы начать проект?

Я всегда открыт для обсуждения новых возможностей и интересных задач.