golang

Файлы прямо в бинаре. Go Embed

  • понедельник, 19 января 2026 г. в 00:00:09
https://habr.com/ru/articles/986210/

Стандартная ситуация - в вашем небольшом проекте на Go есть файлы переводов, картинки, миграции, html/gohtml темплейты, которые всегда должны быть рядом с проектом.

Как бы странным это не казалось, но ни раз я видел, как люди упорно пытаются рядом со сбилженым бинарем положить эти файлики в нужной форме, мучают девопсов и насилуют пайпы. Пошла эта мода скорее из некомпилируемых языков, к примеру Java Script, который на месте своего нахождения и без этого плодит помойку из каталогов.

Краткое, быстрое и элегантное решение — Embed.

Реальная проблема

Кейс — у нас сервис авторизации, который будет отправлять email после

  1. Регистрации

  2. Авторизации

  3. Восстановления пароля

  4. и тд

И для этого используем шаблоны. Чаще всего в таких случаях я видел что-то подобное:

package emails

import (...)

var emailTemplates *template.Template

func LoadEmailTemplates() {
	emailTemplates = template.Must(
		template.ParseGlob("templates/ru/*.gohtml"),
	)

	template.Must(
		emailTemplates.ParseGlob("templates/en/*.gohtml"),
	)
}

Начинаем тестить — пишем cd cmd & go run .

Иии... Ничо не работает. Сразу понимаем проблему — пути все относительны. Тогда запускаем из корня, что остается делать.

После go run cmd/main.go все замечательно. Билдим, заливаем, тестим — все сломалось, получаем от пустых сообщений до паник.

Оказывается, что при go build в стандартном виде, в бинарь не идут файлы, которые не импортируются вашим main и их детьми. Единственный очевидный для нас способ — умолять девопса положить рядом эти файлики и каждый раз их обновлять. А если он добрый, то даже и пайп для этого красивый и удобный напишет. А уже на стенде при очередном корявом rollback этот же девопс будет вспоминать вас и ваших родных.

Как раз тут и поможет Embed

Концепция крайнепроста — мы создаем виртуальную файловую систему, которая будет всегда в нашем бинаре. Сразу к коду:

package emails

import (...)

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

var emailTemplates *template.Template

func LoadEmailTemplates() {
	emailTemplates = template.Must(
		template.ParseFS(
			templates,
			"templates/ru/*.gohtml",
			"templates/en/*.gohtml",
		),
	)
}

Вся магия происходит на 5й строчке — с помощью аннотации директивы компилятора. Мы прямо говорим, чтобы все файлы по данному пути мы себе сохранили при билде в бинарь. Сам доступ к этой FS (File System) будет осуществляться через переменную типа embed.FS

Должен отметить, что встраивать таким образом мы можем не только каталоги и единичные файлы, но и привычные структуры данных: string, []byte и тп.

Немало важной особенностью embed.FS является реализация интерфейса fs.FS, а значит работает со всем стандартным стеком (в т.ч. fstest).

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

Теперь при любом билде наши шаблоны будут всегда рядом.

Но есть у этого и свои минусы

  1. Самое очевидное — наш исполнительный файл значительно толстеет. Не столько это минус, так как чаще всего без нужных импортированных файлов этот бинарь и не нужен. Думаю, что и говорить не надо, что как еще одно S3 хранилище использовать такое нет надобности.

  2. Вся эта виртуальная файловая система выгружается сразу в память. Плохо будет в основном для маленьких контейнеров. Но опять же, часто ли вы постоянно перечитываете, а не храните в виде кеша нужные файлы? Если вам полезен LazyLoad и для вас все это критично, то стоит задуматься.

  3. Embed — compile‑time контракт. Если вам требуется что‑то динамичное, то такой способ вам явно не подойдет (тем не менее, все еще хранить это в файловой системе не самый лучший вариант).

Тем не менее, вы получаете

  1. Атомарность любого деплоя/роллбэка

  2. Самодостаточность бинаря

  3. Явный контракт каталогов (в виде директивы)

  4. Отсутствие надобности очем‑то с кем‑то договариваться — мы теперь сами все сделали)

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

  1. Конфиги (естественно не все и всё)

  2. Шаблоны

  3. Остальную Статику HTML

  4. Миграции

  5. Swagger

  6. Локализация

  7. Тестирование

Заключение

Есть множество флоу хранения миграций, локализаций, шаблонов и остальных статичных файлов (от бд до того же S3), но, часто наблюдая такую картину, решил напомнить всем о существовании прикольного инструмента, написав об этом свою первую статью.

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

Спасибо за прочтение!