Стандартная ситуация - в вашем небольшом проекте на 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), но, часто наблюдая такую картину, решил напомнить всем о существовании прикольного инструмента, написав об этом свою первую статью.

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

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

Комментарии (11)


  1. Hrodvitnir
    18.01.2026 05:26

    А потом делать целый релиз, чтобы убрать опечатку в переводе?


    1. en1yee Автор
      18.01.2026 05:26

      а когда опечатка в коде, релиз не делают? точно такой же кейс


      1. iv_kingmaker
        18.01.2026 05:26

        Нет. Зайти на сервер и поправить условный JSON конфиг можно ручками.


        1. en1yee Автор
          18.01.2026 05:26

          в условиях нормального прода само понятие «Зайти на сервер» считается моветоном. Да к тому же руками что-то на нем делать

          А когда польются конфликты? А расхождения prod ветки и фактической?


          1. iv_kingmaker
            18.01.2026 05:26

            Конфиги, которые меняются постоянно (типа "переводов"), не хранятся рядом с кодом. Не считая дефолтных.


        1. ilving
          18.01.2026 05:26

          Особенно удобно лезть руками в десяток подов )


          1. iv_kingmaker
            18.01.2026 05:26

            И ещё "круче" запускать полное обновление всех сервисов (включая тесты) ради исправления опечатки в переводе, которые по правильному должны были хранится в объединённом хранилище. Куда удобнее зайти в условный S3, исправить опечатку и перезагрузить сервисы (если требуется), нежели запускать обновление со всеми вытекающими последствиями. Бывают сервисы, которые только собираются по несколько часов.


            1. Turbine
              18.01.2026 05:26

              Content delivery? Не, не слышал. Релиз - это дефолтный способ выкатки обновления. Не, если у вас собственный сайт, где вы сам себе хозяин, то вы спокойной можете лезть куда хотите и делать, что нужно. Хотя опять же, лучше настроить cd, чтобы изменения делать через репозиторий, а не прод


  1. riky
    18.01.2026 05:26

    Не работаю с го, но интересно. В дев сборке файлы остаются в файлах реальной ФС или тоже в бинаре?


    1. ilving
      18.01.2026 05:26

      Директива embed включает указанные файлы в бинарник при компиляции. Так что в дев сборке они тоже будут


  1. thepax
    18.01.2026 05:26

    Вся эта виртуальная файловая система выгружается сразу в память.

    Немного не так. Бинарник мапится в память, соответственно загружаются только нужные страницы по мере необходимости. При недостатке свободной памяти они будут выгружены. Как при swap out, только на диск ничего не записывается, так как уже лежит на диске. В общем и целом нагрузка на память и диск будет плюс минус такой же.