Перейти к основному содержимому

Модули, workspace, embed и slog

Разработчику Архитектору

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

См. также: Функции и методы — модули · Первая программа · Рекомендации.


go.mod и кэш модулей

Файл go.mod задаёт путь модуля, версию Go и зависимости. Команда go mod download кладёт модули в кэш (обычно $GOPATH/pkg/mod), общий для всех проектов на машине.

go mod download

Разбор:

  • go mod download скачивает все зависимости, объявленные в go.mod/go.sum, в локальный модульный кэш.

  • Команда не запускает сборку и не правит исходники — она готовит окружение для последующих build/test.

  • Полезно запускать заранее в CI-шаге, чтобы отделить сетевые проблемы от ошибок компиляции.

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

  • go mod tidy — синхронизировать зависимости с импортами;

  • go mod verify — проверить хэши в go.sum;

go mod verify

Разбор:

  • go mod verify сверяет содержимое модулей в локальном кэше с контрольными хэшами из go.sum.

  • Если зависимость была повреждена или подменена, команда сигнализирует об этом до этапа сборки.

  • Это дополнительный контроль целостности supply chain в дополнение к обычному go build.

  • Команда особенно полезна в регламентированных CI/CD контурах и перед релизной сборкой.

  • go get example.com/pkg@v1.2.3 — добавить или обновить зависимость.

go get example.com/pkg@v1.2.3

Разбор:

  • go get в таком виде фиксирует конкретную версию зависимости (v1.2.3) в графе модулей проекта.
  • В результате обновляются go.mod (требуемая версия) и при необходимости go.sum (новые хэши).
  • Явное указание версии повышает воспроизводимость сборки и снижает риск случайного обновления до несовместимого релиза.
  • Подход удобен для контролируемых апгрейдов: сначала точечно обновили пакет, затем прогнали тесты и только потом закоммитили изменения.

go.sum фиксирует криптографические хэши — защита supply chain при повторной сборке.


Каталог vendor

go mod vendor

Разбор:

  • go mod vendor копирует исходники всех реально используемых зависимостей в локальный каталог vendor.
  • Это позволяет собирать проект без обращения к внешним proxy/репозиториям, если использовать режим -mod=vendor.
  • Команда не отменяет роль go.mod: именно он остаётся источником правды о версиях, а vendor — материализованная копия.
  • Практически это полезно в офлайн-CI, изолированных средах и инфраструктуре с жёсткими требованиями к воспроизводимости.

Создаёт копию всех зависимостей в ./vendor. Сборка с флагом -mod=vendor (или переменная GOFLAGS=-mod=vendor) берёт код из vendor, а не из сетевого кэша.

Когда нужен vendorКогда достаточно кэша
CI без доступа в интернетОбычная разработка
Жёсткая воспроизводимость артефактаgo.sum + proxy
Корпоративная политика

vendor не заменяет go.mod: версии по-прежнему объявлены в модуле. replace в go.mod действует и при vendor-сборке.


go work — несколько модулей в одном репозитории

Workspace связывает несколько go.mod без постоянных replace в каждом проекте:

go work init ./services/api ./services/worker ./pkg/common

Разбор:

  • go work init ... создаёт файл go.work в текущем каталоге и сразу добавляет перечисленные локальные модули в workspace.
  • Путь к каждому модулю указывает на директорию с собственным go.mod, которую toolchain должен рассматривать как часть единого рабочего набора.
  • После этого зависимости между модулями резолвятся на локальные каталоги, а не на опубликованные версии из удалённого репозитория.
  • Это ускоряет разработку монорепо: изменения в общей библиотеке моментально видны сервисам без ручных replace в каждом модуле.

Файл go.work:

go 1.22

use (
./services/api
./services/worker
./pkg/common
)

Разбор:

  • Строка go 1.22 фиксирует версию language/toolchain, с которой ожидается работа workspace.
  • Блок use (...) перечисляет локальные модули, объединённые в один контекст сборки.
  • Каждая запись ./... — относительный путь от директории с go.work, поэтому структура репозитория напрямую влияет на корректность резолва.
  • Наличие go.work избавляет от дублирующихся replace и уменьшает риск "забыть обновить" конфигурацию в одном из модулей.

Локальные изменения в pkg/common сразу видны зависимым модулям при go build из корня workspace. Для публикации в CI часто собирают конкретный подмодуль; go.work обычно в .gitignore или коммитят для монорепо команды.

Отличие от replace github.com/me/lib => ../lib в одном go.mod: go work масштабируется на N сервисов без дублирования директив.

Показать, какие модули подключены в workspace:

go work use ./services/api ./pkg/common
go work edit -json

Разбор:

  • go work use ... добавляет (или обновляет) локальные модули в текущем go.work.
  • go work edit -json выводит структуру workspace в машинно-читаемом виде, что удобно для отладки и автоматизации.
  • Такой просмотр помогает быстро понять, почему модуль резолвится локально или, наоборот, тянется из удалённого источника.
  • Полезно применять перед CI, чтобы убедиться, что локальный workspace не маскирует проблемы с версиями в go.mod.

embed — статика в бинарнике

Пакет embed (Go 1.16+) встраивает файлы при компиляции:


import "embed"

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

//go:embed config/default.yaml
var defaultConfig []byte

Разбор:

  • import "embed" подключает специальный пакет стандартной библиотеки, активирующий механику встраивания файлов в бинарник.
  • Директива //go:embed static/* перед переменной говорит компилятору включить соответствующие файлы в embed.FS.
  • var staticFS embed.FS — файловая система "только чтение" внутри бинарника; из неё можно читать статику без внешнего диска.
  • //go:embed config/default.yaml + var defaultConfig []byte встраивает конкретный файл сразу как байтовый срез.
  • В итоге приложение становится самодостаточным: конфиг и ассеты едут вместе с исполняемым файлом, что упрощает деплой.

Ограничения:

  • путь только относительно пакета, без ..;
  • переменная должна быть на уровне пакета;
  • шаблоны в комментарии //go:embed обязательны.

Раздача в HTTP:

http.Handle("GET /static/", http.FileServer(http.FS(staticFS)))

Разбор:

  • http.FS(staticFS) адаптирует embed.FS к интерфейсу файловой системы, который понимает http.FileServer.
  • http.FileServer(...) возвращает HTTP-обработчик, который раздаёт файлы как обычную статику (css/js/img).
  • http.Handle("GET /static/", ...) регистрирует маршрут, на котором эта встроенная статика становится доступной клиентам.
  • Такой подход убирает необходимость монтировать отдельную директорию со статикой в контейнере.
  • Важно корректно выбрать префикс маршрута, чтобы URL и структура встроённых файлов совпадали.

Удобно для SPA, иконок, встроенных миграций SQL без отдельного тома в контейнере.


log/slog — структурированные логи

С Go 1.21 в стандартной библиотеке — log/slog:

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
logger.Info("request done",
"method", r.Method,
"path", r.URL.Path,
"ms", elapsed.Milliseconds(),
)

Разбор:

  • slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{...}) создаёт обработчик, который пишет логи JSON-объектами в стандартный вывод.
  • Level: slog.LevelInfo задаёт порог логирования: Debug будет отфильтрован, а Info и выше — записаны.
  • slog.New(...) оборачивает handler в logger, через который вызываются методы Info/Warn/Error.
  • logger.Info("request done", ...) пишет событие с человекочитаемым сообщением и структурированными полями.
  • Пары "method", "path", "ms" превращаются в отдельные ключи JSON, что облегчает поиск и агрегацию в системах наблюдаемости.
  • elapsed.Milliseconds() явно нормализует длительность в миллисекунды, чтобы аналитика по latency была единообразной.

Пары ключ–значение лучше, чем fmt.Sprintf в сообщении: системы вроде Loki/ELK индексируют поля.

Уровни — Debug, Info, Warn, Error. Контекст:

logger = logger.With("request_id", id)
logger.Error("db", "err", err)

Разбор:

  • logger.With("request_id", id) создаёт производный логгер с контекстным полем, которое автоматически добавится ко всем последующим записям.
  • Это упрощает трассировку одного пользовательского запроса через разные слои сервиса без ручного дублирования request_id.
  • logger.Error("db", "err", err) фиксирует ошибку уровня Error и сохраняет сам объект err как структурированное поле.
  • За счёт единого контекста и формата такие логи удобнее коррелировать с метриками и трейсам.

Свой slog.Handler подключает Zap, Logrus и корпоративные форматы без смены вызовов в коде (интерфейсы).

Пример обогащения логов контекстом запроса:

ctxLogger := logger.With(
"service", "billing-api",
"request_id", reqID,
"user_id", userID,
)
ctxLogger.Info("invoice created", "invoice_id", invoiceID)

Разбор:

  • logger.With(...) формирует "дочерний" логгер с постоянными полями контекста.
  • Поля service, request_id, user_id автоматически попадут во все записи через ctxLogger.
  • Это снижает шум в коде: не нужно каждый раз вручную повторять один и тот же набор ключей.
  • ctxLogger.Info(..., "invoice_id", ...) добавляет уже событийные поля конкретного действия поверх базового контекста.
  • Результат — удобная фильтрация логов по request/user/invoice в системах наблюдаемости.

Практический шаблон конфигурации

Минимальный каркас для сервиса в контейнере:

  1. go.mod и go.sum коммитятся всегда.
  2. go.work используют локально для разработки нескольких модулей.
  3. slog пишет JSON в stdout, сбор логов делает платформа.
  4. embed хранит дефолтные шаблоны и статические файлы.

Такой набор уменьшает различия между локальной средой, CI и production.


CI — GitHub Actions и GitLab Runners

Один и тот же pipeline тестов и сборки можно описать в GitHub Actions и GitLab CI.

GitHub Actions:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- run: go mod download
- run: go test -race -coverprofile=coverage.out ./...
- run: go build -o app ./cmd/app

GitLab CI (Runner с образом Go):

test:
image: golang:1.22
script:
- go mod download
- go test -race ./...
- go build -o app ./cmd/app
artifacts:
paths:
- app

Разбор:

  • go mod download отделяет сетевые ошибки от ошибок компиляции.
  • -race в CI ловит гонки, которые локально забыли включить.
  • Секреты (токены registry) хранят в GitHub Secrets / GitLab CI variables, не в go.mod.
  • Docker-образ Go-приложения — пример микросервиса.

Подробнее про тесты в pipeline — тестирование в Go.


Связка в проекте

Типичный сервис в монорепо:

  1. go.work для локальной разработки библиотек;
  2. go.mod в каждом сервисе для релиза;
  3. go mod vendor в CI при офлайн-сборке;
go mod vendor

Разбор:

  • Повторный вызов go mod vendor в этом месте подчёркивает, что vendor-слой обычно формируют отдельным шагом релизной/CI-подготовки.
  • Команда должна выполняться после обновления зависимостей, чтобы содержимое vendor соответствовало актуальному go.mod.
  • Если vendor хранится в репозитории, этот шаг делает изменения явными в diff и упрощает аудит поставки.
  • В монорепо важно запускать команду в нужном модуле, иначе можно случайно подготовить vendor для другого сервиса.
  1. //go:embed для OpenAPI/UI или дефолтного конфига;
  2. slog + JSON в stdout для Kubernetes.

Подробнее о модулях в коде — функции и методы, первый проект — первая программа.

Также полезно пройти веб на стандартной библиотеке и работу с интерфейсами, чтобы сразу применять slog и context в HTTP-сервисах.


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.