Модули, версионирование и зависимости#
Модуль: Core Go · Уровень: Middle+/Senior
TL;DR#
Модуль — единица версионирования и распространения кода: каталог с go.mod, в котором объявлены путь модуля, версия языка и требуемые зависимости. Версии подчиняются semver, а мажорные версии начиная с v2 обязаны нести суффикс в пути модуля (example.com/lib/v2) — правило импорта совместимости. Разрешение зависимостей выполняется алгоритмом MVS (Minimal Version Selection): выбирается минимальная версия, удовлетворяющая всем требованиям, а не максимальная — отсюда воспроизводимость сборок без lock-файла. Целостность гарантируют go.sum и checksum database, приватные модули выводятся из-под них через GOPRIVATE. go mod tidy приводит требования в соответствие с реальными импортами, replace подменяет модуль локальным путём, go.work объединяет несколько модулей в одну рабочую область.
Простыми словами#
Модуль — это проект целиком: каталог, в котором лежит go.mod со списком того, что проекту нужно.
Главное отличие от привычных менеджеров пакетов прячется в способе выбора версий. В npm или Maven менеджер берёт самую свежую версию, подходящую под ограничения, — поэтому сборка вчера и сегодня может дать разный результат, и нужен lock-файл, который это зафиксирует. Go делает наоборот: он берёт минимальную версию, которая удовлетворяет всем требованиям. Обновление происходит только тогда, когда ты явно его попросил. Отсюда воспроизводимость сборок без отдельного lock-файла.
Второе, что удивляет новичков: мажорная версия входит в путь импорта. Библиотека второй версии — это github.com/foo/bar/v2, и с точки зрения Go это другой пакет. Выглядит непривычно, зато v1 и v2 могут спокойно сосуществовать в одной сборке, и не бывает ситуации «две библиотеки требуют несовместимые мажорные версии третьей».
Если ты с другого языка. go.mod — это package.json или pom.xml, а go.sum — не lock-файл, а список контрольных сумм: он отвечает за «код не подменили», а не за «какая версия выбрана». Виртуальные окружения не нужны: зависимости лежат в общем кэше модулей, а версии определяются файлом проекта.
Словарь этой статьи:
- module path — путь модуля, он же префикс импорта:
github.com/user/repo; - MVS — алгоритм выбора минимальной достаточной версии;
+incompatible— метка для модулей, которые выпустили v2+ без суффикса в пути;replace— подмена модуля другим путём или версией;- workspace (
go.work) — режим одновременной работы с несколькими модулями.
Теория#
go.mod: что внутри#
module github.com/vbloher/orders // путь модуля = префикс импорта
go 1.24 // версия языка (влияет на семантику!)
toolchain go1.24.3 // минимальная версия тулчейна (1.21+)
require (
github.com/jackc/pgx/v5 v5.6.0
golang.org/x/sync v0.8.0
)
require (
github.com/pkg/errors v0.9.1 // indirect
)
replace github.com/foo/bar => ../bar // локальная разработка
exclude github.com/bad/lib v1.2.3 // никогда не выбирать эту версию
retract v1.4.0 // автор помечает свой релиз как отзываемыйКлючевые моменты:
moduleзадаёт префикс, по которому пакеты модуля импортируются. Переименование модуля = смена всех путей импорта у потребителей.go 1.24— не «требуется такой компилятор», а версия языка. От неё зависит семантика (та же переменная цикла в 1.22) и доступность новых конструкций. Это главный механизм обратной совместимости Go.// indirectпомечает зависимости, которые твой код не импортирует напрямую — они нужны твоим зависимостям.
Semver и правило импорта совместимости#
Версии модулей — семантические: vMAJOR.MINOR.PATCH. Смена мажорной версии означает несовместимые изменения.
Go реализует это буквально: начиная с v2, мажорная версия входит в путь модуля.
import "github.com/foo/bar" // v0 или v1
import "github.com/foo/bar/v2" // v2
import "github.com/foo/bar/v3" // v3Следствия:
- v1 и v2 одной библиотеки — разные пакеты, могут присутствовать в сборке одновременно. Это решает проблему «diamond dependency», от которой страдают другие экосистемы;
- переход на v2 для автора библиотеки — не просто тег, а изменение
moduleвgo.mod(и обычно каталог/v2или отдельная ветка); - если автор выпустил тег
v2.0.0, не поменяв путь модуля, Go пометит такую версию как+incompatibleи будет обращаться с ней по старым правилам.
Отдельно: v0 ничего не гарантирует (v0.x.y — «API может измениться в любой момент»), а псевдоверсии вида v0.0.0-20240115120000-abcdef123456 генерируются для коммитов без тегов.
MVS: Minimal Version Selection#
Алгоритм разрешения версий в Go принципиально отличается от npm/Cargo/Maven.
Задача: у модуля A есть требования B v1.2.0 и C v1.0.0, а C требует B v1.3.0. Какую версию B взять?
- Менеджеры «максимальных версий» возьмут самую свежую подходящую — например
B v1.9.5, вышедшую вчера. Результат зависит от даты сборки, поэтому нужен lock-файл. - MVS возьмёт
B v1.3.0— минимальную версию, удовлетворяющую всем требованиям одновременно.
Что это даёт:
- Воспроизводимость по построению. Один и тот же набор
go.modвсегда даёт один и тот же результат, сегодня и через год. Lock-файл не нужен. - Обновления только по запросу. Новая версия зависимости не приедет сама. Её приносит явный
go get -uи коммит вgo.mod. - Предсказуемость релизов библиотек. Выпустив новую версию, ты не ломаешь чужие сборки автоматически.
Обратная сторона: обновления безопасности не прилетают сами — за ними нужно ходить (go get -u, Dependabot, govulncheck).
go.sum и проверка целостности#
go.sum содержит криптографические хеши содержимого каждой используемой версии модуля. Это не lock-файл: он не определяет, какая версия выбрана (это делает MVS по go.mod), а гарантирует, что содержимое выбранной версии не подменили.
Дополнительный слой — checksum database (sum.golang.org): публичный прозрачный лог хешей. Если автор перезапишет тег в репозитории, хеш разойдётся с записанным в логе, и сборка упадёт с ошибкой проверки.
Для приватного кода это надо отключать:
GOPRIVATE=github.com/mycompany/* # не ходить в прокси и sumdb
GONOSUMDB / GONOSUMCHECK # устаревшие варианты
GOFLAGS=-mod=mod # поведение при расхождении go.modGOPROXY по умолчанию — proxy.golang.org,direct: сначала публичный прокси-кэш, потом прямое обращение к репозиторию. Корпоративный прокси (Athens, Artifactory) ставят ради независимости от доступности GitHub и контроля над зависимостями.
Команды, которые нужно знать#
go mod init github.com/user/repo # создать модуль
go mod tidy # синхронизировать go.mod/go.sum с реальными импортами
go mod download # скачать в кэш, ничего не меняя
go mod verify # проверить, что кэш соответствует go.sum
go mod why github.com/foo/bar # кто тянет эту зависимость
go mod graph # полный граф зависимостей
go get github.com/foo/bar@v1.2.3 # добавить/сменить версию зависимости
go get -u ./... # обновить minor/patch версии
go get github.com/foo/bar@none # удалить зависимость
go install github.com/foo/cmd@latest # поставить бинарник, НЕ трогая go.modВажное разделение, появившееся в Go 1.16+: go get работает с зависимостями модуля, go install ставит исполняемые файлы. Раньше обе команды делали и то, и другое, отсюда путаница в старых инструкциях.
go mod tidy — команда, которую запускают перед каждым коммитом: она добавляет недостающие требования, убирает лишние и приводит go.sum в порядок. В CI полезна проверка «после tidy ничего не изменилось».
replace и локальная разработка#
replace github.com/mycompany/lib => ../libКлассический сценарий: правишь библиотеку и сервис одновременно. replace действует только в главном модуле — если твой модуль подключат как зависимость, его replace проигнорируют.
Отсюда правило: replace на локальный путь не должен уезжать в основную ветку. Для постоянной работы с несколькими модулями есть более чистый инструмент.
Workspaces (go.work, Go 1.18+)#
go work init ./service ./libgo 1.24
use (
./service
./lib
)Рабочая область объединяет несколько модулей: сборка видит локальные версии вместо опубликованных, при этом go.mod каждого модуля остаётся чистым. go.work обычно не коммитят (или коммитят вместе с go.work.sum для монорепозитория — это вопрос договорённости команды).
vendor#
go mod vendor копирует все зависимости в каталог vendor/ внутри проекта. Если он есть, сборка по умолчанию идёт из него (-mod=vendor).
Зачем это делают сегодня: сборка без доступа к сети, гарантия наличия кода на случай удаления репозитория, требования аудита. Против: раздутый репозиторий и шумные диффы. С появлением прокси-кэшей необходимость в vendor заметно снизилась.
Уязвимости#
govulncheck ./...Инструмент от команды Go: сверяет зависимости с базой уязвимостей и, что важнее, проверяет, вызывается ли уязвимый код из твоего. Это резко сокращает поток ложных срабатываний по сравнению с обычным сканером версий.
Подводные камни / gotchas#
go.sum— не lock-файл. Он про целостность, а не про выбор версий. Удалять его «чтобы обновиться» бессмысленно и вредно.- Забыли
/v2в пути. Тегv2.0.0без измененияmoduleвgo.modдаёт+incompatibleи ломает ожидания потребителей. replaceне наследуется. Он работает только в главном модуле; в опубликованной библиотеке он бесполезен и вводит в заблуждение.- Директива
goменяет семантику языка. Поднятиеgo 1.21→go 1.22меняет поведение переменной цикла. Это не косметика. go get -uобновляет транзитивно. Обновление всего дерева одной командой перед релизом — хороший способ получить сюрприз; лучше обновлять точечно.- Приватные репозитории. Без
GOPRIVATEтулчейн пойдёт в публичный прокси и sumdb за твоим приватным кодом и получит 404 (а заодно засветит путь модуля). - Тег переписали. Force-push тега в опубликованной библиотеке ломает сборки всем — checksum database это заметит. Правильный путь — выпустить новую версию и
retractстарую. - Крупный монорепозиторий. Несколько модулей в одном репозитории работают, но требуют аккуратных тегов вида
subdir/v1.2.3.
Вопросы на собеседовании#
В: Чем MVS отличается от разрешения зависимостей в npm или Maven?
О: MVS выбирает минимальную версию, удовлетворяющую всем требованиям, тогда как большинство других менеджеров берут максимальную подходящую. Это даёт воспроизводимость без lock-файла: результат зависит только от содержимого go.mod, а не от даты сборки. Ценой становится необходимость обновляться явно.
В: Зачем в пути модуля суффикс /v2?
О: Это правило импорта совместимости: разные мажорные версии — разные пакеты. Благодаря ему v1 и v2 одной библиотеки могут сосуществовать в одной сборке, и конфликт мажорных версий транзитивных зависимостей перестаёт быть проблемой.
В: go.sum — это lock-файл?
О: Нет. Он хранит хеши содержимого версий и защищает от подмены кода. Выбор версий полностью определяется go.mod и алгоритмом MVS.
В: Что делает директива go 1.22 в go.mod?
О: Задаёт версию языка для модуля. От неё зависят доступные конструкции и семантика — например поведение переменной цикла. Компилятор более новой версии всё равно применит к этому модулю правила указанной версии языка.
В: В чём разница между go get и go install?
О: go get управляет зависимостями текущего модуля и правит go.mod. go install pkg@version собирает и ставит исполняемый файл в $GOBIN, не затрагивая go.mod текущего проекта.
В: Как работать с приватными модулями?
О: Указать GOPRIVATE (или GONOSUMDB/GOPROXY) для соответствующих префиксов, чтобы тулчейн не ходил в публичный прокси и checksum database, и настроить доступ к репозиторию (SSH или токен через .netrc/insteadOf).
В: Когда нужен vendor/?
О: Когда требуется сборка без доступа к сети, гарантированная сохранность кода зависимостей или того требует аудит. В остальных случаях прокси-кэш решает те же задачи без раздувания репозитория.
В: Что такое replace и какие у него ограничения?
О: Подмена модуля другим путём или версией. Работает только в главном модуле, поэтому для библиотек бесполезен, а локальные пути не должны попадать в основную ветку. Для постоянной работы с несколькими модулями есть go.work.
На что копают на senior+#
- Понимание MVS не на уровне «берёт минимальную», а на уровне следствий: почему нет lock-файла, почему обновления не прилетают сами, что это даёт при релизе библиотеки.
- Знание правила импорта совместимости и умение объяснить, как оно решает проблему конфликта мажорных версий.
- Разделение ответственности:
go.modвыбирает версии,go.sumи sumdb защищают целостность, прокси отвечает за доступность. - Опыт эксплуатации: приватные прокси, зеркала,
GOPRIVATE,govulncheckв CI, проверка «go mod tidyничего не изменил». - Follow-up: «Что произойдёт, если автор перепишет тег?», «Как выпустить v2 библиотеки?», «Зачем нужна директива
toolchain?», «Чемretractотличается от удаления тега?».