Skip to Content
ConventionsGo-зависимости

Go-зависимости

Правила добавления и поддержки внешних модулей. Каждая зависимость — это чужой код, который ты взял на supply-chain-ответственность. Прежде чем делать go get, пройди vetting.

Содержание

Принципы

  • 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 drivergithub.com/jackc/pgx/v5
Миграцииgithub.com/golang-migrate/migrate/v4
Event busgithub.com/ThreeDotsLabs/watermill + watermill-sql, watermill-kafka, watermill-cqrs
Redis clientgithub.com/redis/go-redis/v9
Validationgithub.com/go-playground/validator/v10
JWT (если нужен)github.com/golang-jwt/jwt/v5
Testing helpersgithub.com/stretchr/testify
Integration containersgithub.com/testcontainers/testcontainers-go
Env-configgithub.com/kelseyhightower/envconfig
OpenTelemetrygo.opentelemetry.io/otel, go.opentelemetry.io/otel/sdk
Prometheus clientgithub.com/prometheus/client_golang
ULIDgithub.com/oklog/ulid/v2
UUID (если нужен)github.com/google/uuid

Не тянуть (есть лучше):

Не надоВместо
github.com/pkg/errorserrors + fmt.Errorf("...%w", err)
github.com/sirupsen/logruslog/slog
go.uber.org/zaplog/slog
github.com/julienschmidt/httproutergithub.com/go-chi/chi/v5
github.com/gorilla/muxgithub.com/go-chi/chi/v5
github.com/jinzhu/gorm, gorm.io/gormpgx + ручной SQL
github.com/lib/pqgithub.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 (v1v2) — почти всегда breaking.

Процесс:

  1. Отдельный PR, не смешивать с feature.
  2. Обновить import path (для semver-v2+ модулей путь включает /v2).
  3. Пройти по breaking-changes релизных notes, починить код.
  4. Прогнать полный test suite с integration: testcontainers Postgres, Kafka.
  5. Canary deploy. 1 pod 1 час → 10% 24 часа → 100%. Смотри http_request_errors_total, go_goroutines, память.
  6. Если регрессия — откат, issue в upstream.

Security response

При появлении CVE в зависимости, которая попадает в prod:

  • P1 (critical / high). Hotfix в тот же день. go get <module>@latest
    • govulncheck, 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>.json

SBOM публикуется:

  • как 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.3 forever — через полгода куча пропущенных 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’ов.
Last updated on