логотип PurpleSchool
логотип PurpleSchool

Руководство по embed в Go

Автор

Олег Марков

Введение

Иногда при разработке на Go возникает задача, когда к приложению нужно подключить дополнительные файлы: статические страницы, изображения, шаблоны для генерации HTML, конфиги и многое другое. Такие файлы важно обеспечить прямо внутри итоговой программы, чтобы не раздавать их отдельными файлами вместе с бинарником и не создавать лишних зависимостей от внешних ресурсов. Это особенно актуально, если вы хотите упростить деплой — всё должно запускаться как единое целое.

До выхода Go 1.16 задача решалась сторонними инструментами — такими как go-bindata, packr и аналогичными утилитами. Теперь с помощью стандартной библиотеки вы просто добавляете файлы в само приложение через пакет embed, и никаких дополнительных шагов не требуется.

В этой статье вы познакомитесь с возможностями пакета embed. Я покажу вам, как он работает, какие задачи решает, покажу примеры кода и дам объяснения важнейших моментов — от указания файлов до извлечения содержимого во время выполнения приложения.

Как работает embed в Go

Пакет embed позволяет встраивать (embed — встраивать, внедрять) содержимое файлов (или даже целых директорий) непосредственно в финальный бинарник вашего приложения. Это значит, что после компиляции все необходимые ресурсы будут частью исполняемого файла, и получить их можно будет с помощью переменных, определённых в Go-коде.

Чтобы начать пользоваться возможностями embed, вам нужно:

  1. Подключить пакет embed (даже если вы не используете функции из него явно).
  2. Объявить переменную одного из специальных типов — string, []byte или embed.FS.
  3. Использовать директиву //go:embed, чтобы указать, какие файлы встраивать.

Давайте разберёмся, как это выглядит на практике.

Подключение пакета

Импорт пакета обязательно, даже если обращаться к нему напрямую не планируется:

import _ "embed" // Импорируем, даже если не обращаемся к функциям пакета напрямую

Это предотвращает ошибку компиляции, если вы используете директивы go:embed.

Какие типы переменных подходят

Директива //go:embed работает только с переменными следующих типов:

  • string
  • []byte
  • embed.FS

С помощью string и []byte можно встраивать только один файл. Если вы хотите встроить несколько файлов или целую директорию, используйте embed.FS.

Простейший пример: встраиваем текстовый файл в строку

Давайте посмотрим, как можно встроить в бинарник текстовый файл:

Пример файла

Пусть у нас есть файл с именем message.txt:

Привет, мир!

Пример встраивания

package main

import (
    _ "embed"
    "fmt"
)

//go:embed message.txt
var message string // Переменная должна быть объявлена сразу после go:embed

func main() {
    fmt.Println(message) // Выводит содержимое файла message.txt
}

Комментарии в коде обращают внимание на важный момент: объявление переменной обязательно должно следовать непосредственно за директивой //go:embed. В противном случае компилятор выдаст ошибку.

Встраивание файла в []byte

Если содержимое файла двоичное, стоит использовать тип []byte:

package main

import (
    _ "embed"
    "fmt"
)

//go:embed image.png
var imageData []byte // Здесь хранятся байты из файла image.png

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

В этом примере вы увидите, как считать размер встроенного изображения.

Встраивание нескольких файлов или директорий (embed.FS)

В случаях, когда нужно встроить несколько ресурсов, обращайтесь к типу embed.FS. Он предоставляет виртуальное файловое дерево только для чтения, которое можно использовать как обычное дерево файлов.

Пример структуры проекта

project/
├── templates/
│   ├── main.html
│   └── sidebar.html
└── main.go

Пример кода

package main

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

//go:embed templates/*
var templatesFS embed.FS // Храним все файлы из папки templates

func main() {
    files, _ := fs.Glob(templatesFS, "templates/*.html") // Получаем список файлов шаблонов
    for _, file := range files {
        data, _ := templatesFS.ReadFile(file)
        fmt.Printf("%s:\n%s\n---\n", file, string(data))
    }
}

Здесь используется функция fs.Glob для поиска файлов по шаблону, а затем ReadFile, чтобы прочесть содержимое каждого шаблона.

Паттерны и маски для go:embed

Вы можете указывать:

  • Отдельные файлы: //go:embed file.txt
  • Директории: //go:embed static/*
  • Несколько путей через пробел: //go:embed a.txt b.txt c/*.json

Поддерживаются маски (*, ?) и относительные пути относительно файла Go-кода, где размещена директива.

Ограничения и особенности пакета embed

Вот нюансы, на которые стоит обратить внимание:

  • Встраиваемые файлы определяются во время компиляции. То есть, если вы после компиляции поменяете embedded-файлы на диске, внутри бинарника они останутся прежними до следующей сборки.
  • embed работает только с файлами, доступными на момент сборки. Не поддерживается загрузка файлов из интернета или по абсолютным путям.
  • Файлы нельзя изменять из программы во время её исполнения — только читать.
  • Файлы игнорируются, если их нет по указанному пути во время компиляции (ошибка компиляции).
  • Путь в директиве всегда относительно файла, где размещён //go:embed. Если структура проекта становится сложной — этот момент часто становится источником ошибок.
  • Встроенные файлы не видны в исходном дереве файлов вашей операционной системы. С ними можно работать только через ваши переменные внутри кода.

Взаимодействие с файловой системой

Встроенное дерево файлов через embed.FS соответствует интерфейсу fs.FS, который стандартный для доступа к абстрактным файловым источникам в Go. Это значит, что функции, которые работают с виртуальными файловыми системами (например, http.FileServer, template.ParseFS), запросто принимают на вход embed.FS.

Вот пример, как встроить статические файлы и раздать их через HTTP:

package main

import (
    "embed"
    "net/http"
)

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

func main() {
    fs := http.FS(staticFS) // Оборачиваем embed.FS в http.Filesystem
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(fs)))
    http.ListenAndServe(":8080", nil) // Сервер будет доступен на http://localhost:8080/static/
}

Теперь содержимое папки static будет раздаваться пользователям по URL http://localhost:8080/static/.

Использование шаблонов (text/template и html/template)

Многие Go-приложения работают с шаблонами, которые раньше лежали на диске. С внедрением embed стало проще держать шаблоны вместе с бинарником.

Пример:

package main

import (
    "embed"
    "html/template"
    "os"
)

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

func main() {
    // Загружаем все шаблоны из встроенной папки templates
    tmpl, err := template.ParseFS(templatesFS, "templates/*.html")
    if err != nil {
        panic(err)
    }
    data := struct{ Name string }{Name: "Мир"}
    tmpl.ExecuteTemplate(os.Stdout, "main.html", data)
}

Этот подход работает и с text/template.

Особенности использования в тестах

Если вы планируете использовать embed в тестах (например, в тестовых данных), структура та же — просто убедитесь, что файлы доступны там, куда ссылается ваш embed-путь.

Совет: стоит хранить тестовые ресурсы в папке рядом с тестовыми файлами, чтобы не было путаницы с путями.

Пример

//go:embed testdata/input.json
var inputData string

Использование относительных и абсолютных путей

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

Встраивание файлов с учётом gitignore

embed не учитывает содержимое .gitignore, т.е. можно встроить даже файлы, которые не залиты в git. Но если файл отсутствует физически на момент билда, возникнет ошибка.

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

Совместимость с кросс-платформенными и кросс-компиляторными сборками

Поскольку embed работает на этапе компиляции, дополнительные настройки для кросс-компиляции не требуются — файлы автоматически включаются в бинарник для нужной платформы.

Заключение

Пакет embed — очень мощный и удобный инструмент для встраивания файловых ресурсов прямо в исполняемый binary-файл на Go. Это решение избавляет от многих типичных проблем с доставкой вспомогательных файлов, минимизирует потребность во внешних зависимостях и делает деплой приложений заметно проще. Теперь вы знаете основные приемы и способы использования, умеете встраивать как одиночные файлы, так и целые директории, понимаете ограничения и правила работы.

Дальнейшее использование embed сводится лишь к аккуратному управлению вашими ресурсами и выбору корректных путей.

Частозадаваемые технические вопросы по теме статьи и ответы на них

Как встроить файлы, которые лежат в разных папках проекта?

В одной директиве можно перечислить несколько вариантов путей через пробел, например: go //go:embed templates/* configs/*.json Главное — все пути должны быть относительными к файлу, где размещается директива.

Как обновить встроенные файлы, если исходники изменились?

Вам нужно пересобрать бинарник. После изменений файлов выполните новую сборку с помощью go build.

Можно ли получить список всех файлов, встроенных через embed.FS?

Да, через функцию ReadDir: go dirs, err := templatesFS.ReadDir("templates") for _, entry := range dirs { fmt.Println(entry.Name()) } Это покажет имена всех файлов и папок внутри папки templates.

Как встроить файл с названием, начинающимся с точки (например, ".env")?

Такие файлы могут быть пропущены, если явно не указаны. Указывайте их напрямую: go //go:embed .env

Как определить MIME-тип или тип содержимого встроенного файла?

Go стандартно не хранит MIME-тип, но тип можно определить вручную, используя сторонние библиотеки, например, net/http для функции DetectContentType: go mimeType := http.DetectContentType(imageData) Где imageData — содержимое файла в виде []byte.

Стрелочка влевоАвтоматизация Golang проектов — CI/CD с GitLab CI и JenkinsОтладка кода в GolangСтрелочка вправо

Постройте личный план изучения Golang до уровня Middle — бесплатно!

Golang — часть карты развития Backend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по Golang

Работа с YAML в GolangПреобразование типов в GolangКонвертация структур в JSON в GolangStrconv в GolangИспользование пакета SQLx для работы с базами данных в GolangРазбираемся с SQL в GolangРазделение строк с помощью функции split в GolangSort в GoПоиск и замена строк в Go - GolangИспользование пакета reflect в GolangРабота с PostgreSQL в GoPointers в GolangПарсинг в GoРабота со списками (list) в GolangПреобразование int в string в GolangРабота с числами с плавающей точкой в GolangРабота с полями в GolangИспользование enum в GolangОбработка JSON в GoЧтение и запись CSV-файлов в GolangРабота с cookie в GolangРегистры в GoКэширование данных в GolangПреобразование byte в string в GolangByte в GoИспользование bufio для работы с потоками данных в GolangДобавление данных и элементов (add) в Go
Логирование в Golang. Zap, Logrus, Loki, GrafanaРабота с Docker-контейнерами в GoИспользование pprof в GolangМеханизмы синхронизации в GolangРабота с пакетом S3 в GolangМониторинг Golang приложений с помощью PrometheusОптимизация проектов на GoПаттерны проектирования в GolangТрейсинг запросов с OpenTelemetry в GoНастройка шины событий NATS NSQ в GoМиграции базы данных в GolangНастройка уровней логирования log levels в GoОркестрация контейнеров Go с Kubernetes + DockerGjGo Playground и компилятор GolangИспользование go mod init для создания модулей GolangРабота с переменными окружения (env) в GolangКоманда go build в GolangАвтоматизация Golang проектов — CI/CD с GitLab CI и JenkinsРуководство по embed в GoОтладка кода в GolangЧтение и использование конфигурации в приложениях на GolangКомпиляция в GolangКак развернуть Go-приложение на облаке AWSАутентификация в Golang
Сетевые протоколы в GoПеременные в GolangЗначения в GolangДженерик %T и его применение в GolangТипы данных в GolangИспользование tls в GolangИспользование tag в структурах GolangSwitch в GoСтроки в GolangРабота с потоками (stream) в GolangSelect в GoРуны в GoРабота с пакетом params в GolangКонвертация строк в числа в GolangNull, Nil, None, 0 в GoНаименования переменных, функций и структур в GoInt в GolangУстановка GolangЧтение и установка HTTP заголовков в GolangMethods в GolangGoLand — IDE для разработки на Golang от JetBrainsОбработка «not found» в GolangFloat в GolangФлаги командной строки в Go (Golang)Запуск внешних команд в GolangОбработка ошибок в GoИспользование defer в GolangЗначения default в GolangГенерация кода в GoФорматирование кода в GolangЧистая архитектура в GolangКаналы (channels) в GolangПолучение body из HTTP запроса в Golang
Открыть базу знаний

Лучшие курсы по теме

изображение курса

Основы Golang

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее