Go-зависимости
Правила добавления и поддержки внешних модулей. Каждая зависимость —
это чужой код, который ты взял на supply-chain-ответственность. Прежде
чем делать go get, пройди vetting.
Содержание
- Принципы
- Как добавить зависимость
- Vetting checklist
- Популярные choices
go.modconventionreplace-директивы- Upgrade (security patches)
- Автоматизация обновлений (Renovate)
- Major version upgrade
- Security response
- Vendor vs module
- SBOM
- CI security checks
- Anti-patterns
- См. также
Принципы
- Stdlib first. Прежде чем тянуть внешний модуль, проверь, нет ли
нужного в стандартной библиотеке.
net/http,log/slog,errors,encoding/jsonзакрывают большую часть задач. - Минимум transitive-зависимостей. Библиотека, которая сама тащит 10+ модулей, — красный флаг. Выбирай узкие фокусированные решения.
- Supply chain-проверка обязательна. Любой новый модуль проходит vetting (см. ниже). Это не бюрократия: одна необдуманная зависимость — один CVE или bootstrap malware в прод.
- Старение зависимостей — баг. Модуль без обновлений полгода +
findings в
govulncheck— апгрейд или замена, а не «оставим пока».
Как добавить зависимость
Последовательность в репозитории сервиса:
go get github.com/<org>/<module>@<version>
go mod tidy
go build ./...
go test ./...
govulncheck ./...
git add go.mod go.sum
git commit -m "feat: добавил <module> для <причина>"Правила:
- Всегда коммить и
go.mod, иgo.sumодним PR.go.sumфиксирует checksum’ы — без него подмена в прокси-кэше пройдёт незамеченной. - Не редактируй
go.modруками — используйgo get/go mod tidy. indirect-секцияgo.modуправляетсяgo mod tidy; вручную туда не пиши.
Vetting checklist
Перед тем, как выполнить go get, пройди по чек-листу:
- Лицензия совместима. MIT, Apache-2.0, BSD-3-Clause, ISC, MPL-2.0 — принимаем. GPL/LGPL/AGPL — нельзя (копилефт вирусится на сервис). Без лицензии — нельзя (юридически нет права использовать).
- Поддерживается. Commits за последние 6 месяцев, issues не висят годами, есть релизы.
- Adoption. Звёзд > 500, либо это well-known модуль (например,
golang.org/x/*). Единичный репозиторий одного автора — повод трижды подумать. - Нет известных CVE.
govulncheck ./...послеgo get— чисто. - Код читаемый. Бегло прошёлся по исходнику, не увидел
obfuscated-фрагментов,
init()-функций с сетевыми вызовами, криптовалютных hook’ов, shell-callout’ов. - Совместим с Go 1.22+.
go.modмодуля не требует более старой версии Go, тесты проходят на нашей версии. - Автор не в denylist. Если известен случай supply-chain-атаки
от этого автора (
event-streamи прочие прецеденты) — не тянем. - Нет альтернативы в stdlib. Финальный sanity-check.
Если хотя бы один пункт не сошёлся — обсуждай в PR, прежде чем добавлять.
Популярные choices
Текущий стек. Перед тем как взять альтернативу любому пункту — обосновывай в PR.
| Область | Модуль |
|---|---|
| HTTP-роутер | github.com/go-chi/chi/v5 |
| Postgres driver | github.com/jackc/pgx/v5 |
| Миграции | github.com/golang-migrate/migrate/v4 |
| Event bus | github.com/ThreeDotsLabs/watermill + watermill-sql, watermill-kafka, watermill-cqrs |
| Redis client | github.com/redis/go-redis/v9 |
| Validation | github.com/go-playground/validator/v10 |
| JWT (если нужен) | github.com/golang-jwt/jwt/v5 |
| Testing helpers | github.com/stretchr/testify |
| Integration containers | github.com/testcontainers/testcontainers-go |
| Env-config | github.com/kelseyhightower/envconfig |
| OpenTelemetry | go.opentelemetry.io/otel, go.opentelemetry.io/otel/sdk |
| Prometheus client | github.com/prometheus/client_golang |
| ULID | github.com/oklog/ulid/v2 |
| UUID (если нужен) | github.com/google/uuid |
Не тянуть (есть лучше):
| Не надо | Вместо |
|---|---|
github.com/pkg/errors | errors + fmt.Errorf("...%w", err) |
github.com/sirupsen/logrus | log/slog |
go.uber.org/zap | log/slog |
github.com/julienschmidt/httprouter | github.com/go-chi/chi/v5 |
github.com/gorilla/mux | github.com/go-chi/chi/v5 |
github.com/jinzhu/gorm, gorm.io/gorm | pgx + ручной SQL |
github.com/lib/pq | github.com/jackc/pgx/v5 |
go.mod convention
module github.com/<org>/<service>-service
go 1.22
require (
github.com/go-chi/chi/v5 v5.x.x
github.com/jackc/pgx/v5 v5.x.x
// ...
)Правила:
- Module path соответствует hosting’у репозитория сервиса.
- Go-версия —
1.22или новее, синхронно между всеми сервисами. Повышаем одновременно или в пределах одного квартала. - Indirect-секцию управляет
go mod tidy, вручную не правим. replace-директивы допустимы только для локальной разработки и должны удаляться до merge. Вmain— никакихreplace. Детали — §replace-директивы.
replace-директивы
replace в go.mod подменяет публикованный модуль на локальный
путь или форк. Это valid mechanism, но в main — запрещён.
Причины:
- Скрывает supply-chain state.
govulncheckзапускается на replacement’е, а не на реальном модуле → CVE в upstream’е не виден. В день, когда replace-директива забыта и убрана, prod получает уязвимую версию. - Читается только теми, кто знает
replace. Новый разработчик смотритgo.mod, видитrequire foo v1.2.3, думает «это и есть foo v1.2.3», и три часа отлаживает разницу с документацией. - Ломает
go install <module>@version. Внешние инструменты, пытающиеся воспроизвести модуль, получают другую версию, чем у нас в сервисе.
Когда replace действительно нужен:
| Сценарий | Правильный путь |
|---|---|
| Локальная разработка библиотеки рядом с сервисом | replace + никогда не commit (оставить в .git/info/exclude или branch-only; удалить перед PR) |
| Hotfix bug в upstream’е, upstream не отвечает | Fork под github.com/<our-org>/<module>, publish, указать в require обычным путём. replace не использовать. |
| Патч с ограниченным scope для одного сервиса | Отдельный branch-PR, где replace помечен TODO-комментарием с тикет-link’ом и deadline’ом (≤ 30 дней). Срок истёк — либо fork, либо upstream merged, либо замена зависимости. |
Тестирование pre-release (@v1.2.3-rc1) | go get github.com/foo/bar@v1.2.3-rc1 — прямо, не replace. |
Правило: если replace-директива живёт в main — это review-блокер.
При merge-инциденте (кто-то всё-таки замержил) заводим ticket на
немедленное удаление.
Исключение — один репозиторий с несколькими Go-модулями (если
такой появится), где replace на локальные пути используется
штатно. Сейчас у нас такой топологии нет (каждый сервис живёт в
отдельном репозитории, не в общем), и правило выше — absolute.
Upgrade (security patches)
Minor/patch-обновление:
go get -u github.com/<module>
go mod tidy
go test ./...
govulncheck ./...Правила:
- Читай release notes. Даже patch может нести breaking в документе (deprecation warning, изменение defaults).
- Прогоняй тесты. Unit + integration.
- Прогоняй
govulncheck ./...после каждого upgrade’а. - Один PR = один модуль для значимых апгрейдов. Батч-апгрейды — только если все модули обновляются до patch’а.
Массовый ежемесячный sweep: go list -u -m all → отдельный PR на
каждую цепочку зависимостей, где есть security fix.
Автоматизация обновлений (Renovate)
Ручной ежемесячный sweep работает, но легко пропускается под нагрузкой. Для каждого сервис-репо настроен Renovate — он открывает PR’ы на обновления зависимостей автоматически.
Конфиг
renovate.json в корне сервис-репо:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":semanticCommits",
":timezone(Asia/Almaty)",
":dependencyDashboard"
],
"schedule": ["before 6am on monday"],
"prConcurrentLimit": 3,
"packageRules": [
{
"matchUpdateTypes": ["patch", "minor"],
"matchPackagePatterns": ["^github.com/", "^golang.org/x/"],
"automerge": false,
"groupName": "go minor/patch"
},
{
"matchUpdateTypes": ["major"],
"automerge": false,
"labels": ["major-upgrade"],
"dependencyDashboardApproval": true
},
{
"matchDepTypes": ["security"],
"prPriority": 10,
"schedule": ["at any time"]
}
],
"vulnerabilityAlerts": {
"enabled": true,
"labels": ["security"]
}
}Что делает:
- Security patches — PR’ы open в real-time, не раз в неделю.
- Patch/minor — раз в неделю, понедельник 06:00 по локальному времени. Группируются в один PR на все Go-модули недели, чтобы не засыпать ревьюеров десятком мини-PR’ов.
- Major upgrades — под manual approval через dependency dashboard (GitHub issue с чеклистом). Никакого автомержа.
- Auto-merge запрещён. Любой upgrade проходит ревью + CI
(
govulncheck+ тесты). Bot не мержит сам.
Правила для ревьюера Renovate PR
Те же, что для ручного upgrade:
- Release notes прочитаны на breaking changes.
-
go.sumобновлён вместе сgo.modв том же commit’е. -
govulncheckзелёный. - CI (unit + integration) зелёный.
- Нет
replace, случайно введённого Renovate (он иногда добавляет, если upstream-CVE).
Если PR висит неделю без ревью — Renovate его сам закроет (по конфигу). Стратегия «зелёный CI → сразу merge» — плохая, ревьюер должен прочитать release notes.
Когда отключать
- На сервисах, где дефолтный stack fixed (legacy, support-only) и
upgrade’ы не выполняются — в
renovate.jsonдобавить"enabled": false. Но тогда лучше закрыть сервис на retirement- track (см.../services-catalog), а не замораживать надолго.
Major version upgrade
Major (v1 → v2) — почти всегда breaking.
Процесс:
- Отдельный PR, не смешивать с feature.
- Обновить import path (для semver-v2+ модулей путь включает
/v2). - Пройти по breaking-changes релизных notes, починить код.
- Прогнать полный test suite с integration: testcontainers Postgres, Kafka.
- Canary deploy. 1 pod 1 час → 10% 24 часа → 100%. Смотри
http_request_errors_total,go_goroutines, память. - Если регрессия — откат, issue в upstream.
Security response
При появлении CVE в зависимости, которая попадает в prod:
- P1 (critical / high). Hotfix в тот же день.
go get <module>@latestgovulncheck, canary, deploy. PR merge через expedited review.
- P2 (medium). В течение недели. Обычный PR-flow, но приоритет выше обычных задач.
- P3 (low). В ближайшем sweep-PR.
Если upstream не отвечает / не фиксит критичный CVE:
- Fork. Создай
github.com/<your-org>/<module>с patch’ем, укажи в PR ссылку на upstream issue и apologize в code comment. - В
go.modиспользуется fork через обычный путь (fork публикуется с правильным module path), не черезreplace.replace— не для долгоживущих состояний.
Vendor vs module
- Module-only — стандарт.
go.mod+go.sumдостаточно, Go-proxy кэширует артефакты. CI и прод скачивают модули из прокси. - Vendor — только если нужен air-gapped build (нет доступа в
интернет из build-окружения). В текущем стеке не используется. Если
потребуется — добавляется в сервис-репо через
go mod vendorи CI-проверкаgo mod verify.
Смешивать режимы (vendor в одном сервисе, не-vendor в другом) — нельзя.
SBOM
SBOM (Software Bill of Materials) — машиночитаемый список всех модулей и их версий, из которых собран артефакт. Нужен для:
- Incident response. «Pwn’ули library X в version Y» → grep по
SBOM’ам всех сервисов, не по
go.sumкаждого репо. - Vulnerability scanning postрелиз.
grype sbom.jsonпроверяет SBOM против CVE DB; вне CI, на свежие данные. - Compliance/audit. Запросы «какие зависимости в сервисе X на
дату Y» — ответ по SBOM’у релиза, а не
git checkoutтого тега.
SBOM как артефакт релиза
На каждый git-tag vX.Y.Z сервиса CI генерирует SBOM из Docker-
образа (не из исходников — важно, чтобы SBOM соответствовал
реально задеплоенному):
syft packages <image-ref> -o spdx-json > sbom-<tag>.jsonSBOM публикуется:
- как release asset в GitHub release (или эквивалент),
- в artifact registry рядом с Docker-образом (если registry поддерживает — Harbor, Artifactory),
- retention — минимум как у Docker-образа сервиса (обычно 6–12 месяцев).
SBOM в pull request (опционально)
Для больших dependency-update PR’ов полезно автоматически прикладывать diff SBOM’а: «добавилось 3 модуля, один из них X не был в предыдущем SBOM’е». CI-job:
syft dir:. -o spdx-json > sbom-pr.json
diff <(jq '.packages[].name' sbom-base.json | sort) \
<(jq '.packages[].name' sbom-pr.json | sort)Не обязателен, но ускоряет review: ревьюер сразу видит, если притащили transitive dependency без упоминания в PR description.
Сканирование SBOM
Раз в неделю CronJob сканирует SBOM’ы всех production-релизов
через grype:
grype sbom:sbom-<tag>.json --fail-on highНайденная high/critical уязвимость в уже задеплоенном релизе —
ticket в backend-канал, приоритет P1. Это второй рубеж после
govulncheck (который смотрит символы в коде) — grype видит все
версии модулей, даже те, что не вызывают уязвимый код, но всё
равно загружены в образ.
CI security checks
В пайплайне сервиса должны стоять следующие проверки (blocker’ы):
govulncheck ./...— блокирует merge при known CVE.go mod verify— checksum’ы вgo.sumсоответствуют скачиваемым модулям.go mod tidy -diff— нет лишних или недостающих записей вgo.mod/go.sum(gated:go mod tidyв CI должен быть no-op).trivy fs --scanners vuln .— дополнительный layer (ловит CVE, которыеgovulncheckне видит из-за слабого symbol-coverage).
SBOM — как артефакт релиза:
syft . -o spdx-json > sbom.jsonПрикладывается к каждому tagged-релизу сервиса.
Anti-patterns
go getбез коммитаgo.sum. Checksum’ы нет — кэш прокси может отдать другую версию модуля, CI этого не заметит.- Игнор
govulncheck-findings. «Это не на hot-path» — так не работает: findings это known CVE в символах, которые ты дотащил. Фикси или reproducible-exclude с обоснованием. - Pin на точную patch-версию без апгрейда.
v1.2.3forever — через полгода куча пропущенных security-patch’ей. - Зависимости на
v0.x. По semver-гарантий нет, breaking change возможен в любой patch. Тянем только если альтернативы нет, и в PR — явный комментарий «после стабилизации апгрейд». - Shell-callout’ы в Go-библиотеках. Если библиотека вызывает
внешние бинарники (
exec.Command("sh", "-c", ...)) — это canary: посмотри код и аргументы руками, или выбери другую библиотеку. - «Глубокие» вендоры. Модуль, сам зависящий от десятков
v0.xи forked-репо других авторов. Одно падение любого — сломанный build сервиса. replaceвmain.replaceпереопределяет публикованный модуль на локальный/форкнутый. Долгоживущееreplaceскрывает отgovulncheckнастоящее состояние, и следующий разработчик не поймёт, откуда берётся код.
См. также
security— supply chain вписан в общую security-модель.commits-and-prs— формат commit-message для upgrade-PR’ов.