Блог


03 Apr 2026

Как встраивать статические ресурсы в бинарные файлы 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() для создания чистых префиксов путей
  • Рассмотрите использование тегов сборки для режима разработки
  • Встраивайте конфигурационные значения по умолчанию, переопределяя их во время выполнения
  • Учитывайте размер бинарного файла при работе с крупными ресурсами
12 Jan 2026

🚀 Запускаем нейросети (LLM) на AMD GPU в Proxmox: гайд для начинающих

Привет, коллега! 👋

Если ты хочешь запустить локальную нейросеть (например, через Ollama) на своём домашнем сервере с Proxmox и видеокартой AMD — ты по адресу. В этой статье разберём по шагам, как «подружить» AMD GPU, ROCm и LXC-контейнер, даже если ты не гуру виртуализации.

18 Nov 2025

Как увеличить размер диска в Proxmox VE: пошаговое руководство

Виртуализация стала неотъемлемой частью современной ИТ-инфраструктуры, и Proxmox VE — одна из самых популярных платформ для управления виртуальными машинами. Однако при активном использовании ресурсов часто возникает необходимость расширить дисковое пространство виртуальной машины. Хотя сам Proxmox позволяет легко увеличить объём виртуального диска через веб-интерфейс, этого недостаточно: операционная система внутри виртуальной машины не всегда автоматически распознаёт новые размеры. В этой статье мы рассмотрим, как корректно расширить раздел файловой системы после увеличения диска в Proxmox VE.

31 Jan 2025

Зачем нужны программисты?

В современном мире цифровые технологии проникают в практически все сферы жизни: от домашних устройств и медицинского оборудования до управления бизнесом и образования. В этом контексте роль программистов становится критически важной.

11 Mar 2024

Отладка заголовков в Nginx

Сервер Nginx позволяет передавать заголовки при проксировании через директивы:

Синтаксис:    proxy_set_header поле значение;
Умолчание:
proxy_set_header Host $proxy_host;
proxy_set_header Connection close;
Контекст:    http, server, location

Выглядит в конфиге это примерно так:

location / {
    proxy_pass http://headers;
    proxy_set_header X-FORWARDED-FOR $remote_addr;
}

Я столкнулся с тем, что не всегда и не все заголовки передаются, но об этом позже. Отсюда возник вопрос: каким образом определить на проксирующем nginx, какие переданы заголовки.

18 Jan 2018

Работа с API криптобиржи Yobit на golang - Получение собственного баланса

Все примеры работают с go version go1.9.2 linux/amd64

Работа с API для торгов

Получение своего баланса

Прежде всего для работы с api торгов нужно создать пару Ключ - Секрет на странице https://yobit.io/ru/api/keys/

Все будет немного сложнее, чем в предыдущем примере. Именно авторизация на бирже и подпись параметров вызвали у меня больше всего вопросов. Потому-то и хочется оставить рабочий код здесь.

Для начала каждый запрос нам нужно будет подписывать. И чтобы запросы (и подписи) отличались, биржа требует указывать каждый раз параметр nonce в виде целого числа в диапазоне 1..2147483646. При чем в каждом следующем запросе это число должно быть больше, чем в предыдущем. Для обнуления нужно создать новый ключ.

Для получения параметра nonce напишем отдельную функцию, чтобы мы могли пользоваться им даже после рестарта приложения:

import (
    "io/ioutil"
    "strconv"
)

func GetNonce(key string) (nonce int, err error) {
    nonceFileName := "nonce." + key[0:8] + ".txt"
    nonceBytes, err := ioutil.ReadFile(nonceFileName)
    if err == nil {
        nonce, _ = strconv.Atoi(string(nonceBytes))
    }
    nonce++
    err = ioutil.WriteFile(nonceFileName, []byte(strconv.Itoa(nonce)), 0644)

    return
}

Для того, чтобы хранить параметр для каждого ключа отдельно, используем часть ключа как часть имени файла.

18 Jan 2018

Работа с API криптобиржи Yobit на golang - Получение доступных пар

Все примеры работают с go version go1.9.2 linux/amd64

Работа с открытым API

Получение информации о доступных валютных парах

Посмотрим, что написано в документации:

Пример запроса: https://yobit.net/api/3/info

Пример ответа:

{
    "server_time":1418654531,
    "pairs":{
        "ltc_btc":{
            "decimal_places":8,
            "min_price":0.00000001,
            "max_price":10000,
            "min_amount":0.0001,
            "hidden":0,
            "fee":0.2
        }
        ...
    }
}

В первую очередь для работы с json-api нам потребуется создать структуры данных, в котрые мы будем разбирать полученный JSON.

20 Jul 2016

Настройка мониторинга php-fpm через Datadog

Для настройки мониторинга необходимо внести некоторые изменеия в pool, который собираемся мониторить, настройку хоста в nginx и подключить конфиг агенту datadog.

Для начала откроем файл пула и раскоментируем следующие строки:

pm.status_path = /status
ping.path = /ping
ping.response = pong
11 Jun 2016

Используем в Ubuntu sudo без пароля

Если вам нужно часто управлять вашим домашним компьютером или сервером, может надоедать постоянно вводить пароль с командой sudo. Решение этого вопроса нашлось на .

Достаточно открыть терминал и ввести команду:

sudo visudo

после чего в последний раз ввести пароль.

Эта команда откроет конфигурацию sudo, в которую внизу нужно прописать:

username ALL=(ALL) NOPASSWD: ALL

где username - это имя вашего пользователя.

02 Jun 2014

Сравнение 2х основных утилит для сжатия статических файлов

Решил сравнить способности по сжатию javascript у 2х основных утилит от google и от yahoo.

Будем сравнивать с несжатой и сжатой версией jquery:

java -jar compiler.jar --js jquery-1.11.1.js --js_output_file=jquery-1.11.1.cc.js
java -jar compiler.jar --compilation_level ADVANCED_OPTIMIZATIONS --js jquery-1.11.1.js --js_output_file=jquery-1.11.1.cca.js
java -jar yuicompressor.jar jquery-1.11.1.js -o jquery-1.11.1.yui.js
jquery-1.11.1.js282766
jquery-1.11.1.min.js95786
jquery-1.11.1.cca.js87933
jquery-1.11.1.cc.js96284
jquery-1.11.1.yui.js107452

Лучше всего сжатие у closure compiler с опцией ADVANCED_OPTIMIZATIONS. На следующем месте идет сжатый jquery от вендоров, так что если под рукой нет компрессора с java, лучше использовать его.

Как ни странно, мой любимый yui compressor на последнем месте. Но, в его защиту можно сказать, что он оставляет все комментарии с копирайтами, удалив которые, наверняка, можно было бы сэкономить еще. А так же, closure compiler не умеет сжимать css а размер его jar-файла 6 856 128 байт, против 891 148 yui, что в 7,69 раз больше.

Так что yui compressor по прежнему остается не заменимой утилитой, которую можно таскать и вместе с проектом в git, а так же сжимать как стили, так и скрипты, пусть и не с максимальной экономией.


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

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