Привет, я Николай.
Senior Golang Developer

Разрабатываю высоконагруженные микросервисы, проектирую архитектуру распределенных систем и оптимизирую производительность баз данных.

Golang Microservices PostgreSQL Redis Apache Kafka Docker Kubernetes gRPC REST API CI/CD Linux Git
Developer Portrait 2
Developer Portrait 2
Developer Portrait 2
Developer Portrait 2

Опыт работы

2020 — 2026

Senior Golang Developer

Data-Driven Lab | Forex Broker

Ведущий разработчик команды микросервисов. Разделение монолита на Go, использование gRPC, оптимизация SQL запросов.

Подробнее →
2020 — 2020

Senior Golang Developer

Маркетплейс Goods.ru

Разработка REST API, работа с очередями сообщений (RabbitMQ/Kafka), настройка CI/CD пайплайнов.

Подробнее →
2018 — 2020

Senior PHP/Golang Developer

Маркетплейс Wellmax.eu

Поддержка legacy кода, написание скриптов автоматизации, работа с базами данных.

Подробнее →

Блог

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

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

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

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

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

habr-career linked in