Модули, 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 в системах наблюдаемости.
Практический шаблон конфигурации
Минимальный каркас для сервиса в контейнере:
go.modиgo.sumкоммитятся всегда.go.workиспользуют локально для разработки нескольких модулей.slogпишет JSON вstdout, сбор логов делает платформа.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.
Связка в проекте
Типичный сервис в монорепо:
go.workдля локальной разработки библиотек;go.modв каждом сервисе для релиза;go mod vendorв CI при офлайн-сборке;
go mod vendor
Разбор:
- Повторный вызов
go mod vendorв этом месте подчёркивает, что vendor-слой обычно формируют отдельным шагом релизной/CI-подготовки. - Команда должна выполняться после обновления зависимостей, чтобы содержимое
vendorсоответствовало актуальномуgo.mod. - Если
vendorхранится в репозитории, этот шаг делает изменения явными в diff и упрощает аудит поставки. - В монорепо важно запускать команду в нужном модуле, иначе можно случайно подготовить vendor для другого сервиса.
//go:embedдля OpenAPI/UI или дефолтного конфига;slog+ JSON в stdout для Kubernetes.
Подробнее о модулях в коде — функции и методы, первый проект — первая программа.
Также полезно пройти веб на стандартной библиотеке и работу с интерфейсами, чтобы сразу применять slog и context в HTTP-сервисах.
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.