Система модулей Go выглядит обманчиво просто: go mod init, go build — и зависимости «как-то подтянулись». Но за этой простотой стоят два нетривиальных инженерных решения — семантический импорт версий и minimal version selection, — которые делают сборки Go воспроизводимыми без lock-файла и решателя ограничений. Если понимать, почему Go выбирает именно эти версии, перестаёшь бороться с инструментом и начинаешь им пользоваться: осознанно ставить replace, чинить конфликт версий, собирать герметично.
Эта статья — про модель зависимостей Go «изнутри»: что на самом деле хранят go.mod и go.sum, чем правило major-версии в пути импорта отличается от привычного semver, почему MVS даёт предсказуемость там, где менеджеры на диапазонах дают дрейф, и как всем этим управлять командами go. Примеры кода — иллюстративные фрагменты go.mod и команд, отдельного стенда под тему тулинга нет.
В статье
- Модуль, go.mod и go.sum
- Семантический импорт версий
- Minimal Version Selection
- Рабочие команды
- replace и exclude
- retract: отзыв версий
- Vendoring
- Workspaces (go.work)
- Публикация своего модуля: теги и свой домен
- Прокси и приватные модули
- Что дальше
Модуль, go.mod и go.sum
Модуль — это единица версионирования и распространения в Go: дерево пакетов с общим корнем, у которого есть файл go.mod. Пакет — единица компиляции (каталог с .go-файлами одного package); модуль — набор пакетов, версионируемый и публикуемый как целое. Один репозиторий обычно = один модуль, хотя в корне могут жить несколько (мультимодульный репозиторий).
Файл go.mod — манифест модуля. Минимальный содержит три вещи: путь модуля, требуемую версию языка и список прямых зависимостей.
module github.com/khorost/example
go 1.26
require (
github.com/jackc/pgx/v5 v5.6.0
golang.org/x/sync v0.8.0
)
require (
github.com/jackc/puddle/v2 v2.2.1 // indirect
)moduleзадаёт путь модуля (module path) — префикс, под которым импортируются все его пакеты. Это же и адрес, по которому модуль скачивается (VCS-путь).go— начиная с Go 1.21 это не «пожелание», а минимальная требуемая версия языка и тулчейна: если вашgoстарее, он откажется собирать модуль (или, по правилам ниже, доскачает нужный). Влияет и на доступные фичи языка, и на дефолты поведения самогоgo(например, автоматический-mod=vendorпри наличииvendor/включился сgo 1.14).toolchain(Go 1.21+) — необязательная директива с конкретной версией тулчейна для сборки (toolchain go1.26.1), которая может быть выше строкиgo. Если установленный тулчейн старее требуемого, командаgoпо умолчанию сама скачает и запустит нужную версию. Поведением рулитGOTOOLCHAIN:auto(дефолт — доскачивать по необходимости),local(использовать только установленный и упасть, если версии не хватает), либо жёсткая фиксация вродеgo1.26.1. Так Go 1.21 развёл два прежде слитых смысла: минимум языка (go) и фактический тулчейн сборки (toolchain).require— прямые и косвенные зависимости с их версиями. Пометка// indirectозначает, что зависимость не импортируется напрямую кодом модуля, а нужна транзитивно (или её версия зафиксирована явно, хотя прямого требования нет).
Ключевая идея: go.mod перечисляет минимально необходимые версии, а не диапазоны и не «что получилось». Это манифест намерений, а не снимок результата.
Файл go.sum — про целостность, а не про выбор версий. На каждую версию модуля в нём обычно две строки: хеш содержимого модуля и отдельно хеш его go.mod.
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAe...=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:aP5A0jLGZQD9glPTKrsd+2rvWyi...=Хеши (алгоритм h1:, поверх SHA-256) вычисляются при первой загрузке и потом сверяются при каждой сборке: если содержимое версии в кеше или в сети не совпало с записанным — go откажется собирать. Дополнительно версии сверяются с публичной checksum database (sum.golang.org), чтобы обнаружить подмену модуля на стороне источника. go.sum если он появился — а он появляется, как только есть хоть одна зависимость, тянущаяся по сети, — нужно коммитить в репозиторий: это гарантия, что все собирают ровно тот код, который прошёл проверку. (У модуля без внешних зависимостей — или где всё заменено локальными replace — go.sum может быть пустым или отсутствовать вовсе, и это нормально.) Управляют этими файлами не руками, а командами go (см. ниже).
Семантический импорт версий
Версии модулей — это semver-теги вида vMAJOR.MINOR.PATCH (обязательный префикс v): v1.4.2, v0.8.0, v2.0.0. Semver задаёт контракт совместимости: patch — багфиксы, minor — обратно совместимые добавления, major — ломающие изменения.
Go делает из этого контракта жёсткое техническое правило, которого нет в большинстве экосистем, — semantic import versioning (SIV). Формулируется как import compatibility rule:
Если у старого и нового пакета одинаковый путь импорта, новый пакет должен быть обратно совместим со старым.
Отсюда следствие: раз major-версия по определению ломает совместимость, у неё обязан быть другой путь импорта. Реализовано это добавлением суффикса major-версии к пути модуля, начиная с v2:
import (
"github.com/jackc/pgx/v4" // модуль major-версии v4
"github.com/jackc/pgx/v5" // major-версия v5 — ДРУГОЙ путь
)Правила:
v0иv1суффикс не получают.v0.x— нестабильный API (semver не даёт гарантий),v1— путь без суффикса.v2и выше обязаны иметь суффикс/vNи вmodule-строке своегоgo.mod(module github.com/foo/bar/v2), и во всех путях импорта.
Практический смысл радикален: разные major-версии — это, с точки зрения Go, разные модули. Их можно импортировать одновременно в одной сборке, они не конфликтуют. Это снимает «diamond dependency» проблему на major-уровне: если библиотека A тянет lib/v1, а библиотека B — lib/v2, обе версии спокойно сосуществуют, каждая по своему пути. Плата — при выпуске v2 автору приходится осознанно менять module-путь, а не просто ставить тег.
Minimal Version Selection
Вот место, где Go расходится почти со всеми: как из требований разных модулей выбирается итоговая версия каждой зависимости. Большинство менеджеров (npm, Cargo, pip с диапазонами) решают SAT-подобную задачу: находят новейшую версию, удовлетворяющую всем ограничениям-диапазонам. Детали resolver’ов у них отличаются, но общий контраст один — диапазоны плюс lock-файл против конкретных минимальных требований. Go делает противоположное — применяет minimal version selection (MVS).
Принцип MVS формулируется в одну фразу: для каждой зависимости выбирается минимальная версия, которая удовлетворяет всем требованиям. «Минимальная, удовлетворяющая всем» — это максимум из нижних границ. Если модуль A требует lib v1.2.0, а модуль B требует lib v1.4.0, MVS берёт v1.4.0 — самую низкую версию, которая при этом не ниже ни одного из требований. Не v1.5.0, вышедшую вчера; не «latest». Ровно то, что запрошено.
Алгоритм построения списка сборки (build list):
- Начать с прямых требований главного модуля.
- Для каждого требования рекурсивно прочитать
go.modэтой версии и собрать её требования. - Для каждого уникального модуля взять максимум из всех запрошенных версий.
- Полученный набор — и есть версии, которые пойдут в сборку.
Диапазонов в go.mod нет вообще — только конкретные минимальные версии. Отсюда свойства, ради которых MVS и придуман:
- Воспроизводимость без lock-файла. Результат — чистая функция от набора
go.modв графе. Нет решателя с эвристиками, нет «повезло с порядком установки».go.sumфиксирует целостность, но не выбор — выбор уже детерминирован самимgo.mod. - Предсказуемость и «high-fidelity builds». Обновление приходит только тогда, когда кто-то явно поднял требование в своём
go.mod. Новый релиз зависимости не втягивается в вашу сборку сам собой — в отличие от менеджеров на диапазонах, где^1.2.0завтра молча подтянет1.9.0. Вы собираете максимально близко к тому, что тестировал автор. - Минимум сюрпризов при обновлении. «Поднять версию» — это осознанное действие (
go get), а не побочный эффект переустановки.
Контраст стоит проговорить прямо: SAT-решатель оптимизирует под «новее» и потому нуждается в lock-файле, чтобы зафиксировать «повезло». MVS оптимизирует под «ровно как заказано» и потому lock-файл ему не нужен — детерминизм встроен в саму модель. Обратная сторона — свежие версии не приезжают сами; за обновлениями (в том числе security) нужно ходить командами явно.
Рабочие команды
Небольшой набор команд закрывает почти всё повседневное управление зависимостями.
# добавить/обновить конкретную зависимость (пишет в go.mod и go.sum)
go get github.com/jackc/pgx/v5@v5.6.0
go get github.com/jackc/pgx/v5@latest
# обновить прямые и косвенные зависимости до новых minor/patch
go get -u ./...
# только патчи (без подъёма minor)
go get -u=patch ./...
# привести go.mod/go.sum в порядок: добавить недостающее, убрать лишнее
go mod tidy
# скачать модули в локальный кеш (без сборки)
go mod download
# показать полный список сборки (все модули, что реально войдут)
go list -m allЧто делает каждая:
go get pkg@version— меняет требование к модулю вgo.mod(и обновляетgo.sum). Версию можно задать тегом (@v5.6.0), псевдонимами@latest/@upgrade/@patch, коммитом или веткой. Важно: начиная с Go 1.16,go getбольше не ставит бинарники — для разовой установки исполняемого инструмента в$GOBINиспользуетсяgo install pkg@version. А для инструментов, которые нужны самому проекту (кодогенераторы, линтеры,stringerи т.п.), с Go 1.24 есть отдельный механизм:go get -tool pkg@versionзаписывает инструмент директивойtoolвgo.mod(версия закреплена и воспроизводима, как у обычной зависимости), а запускают его черезgo tool <имя>. Это заменяет старый приём сtools.goи пустым импортом — раньше зависимость-инструмент держали живой только фиктивнымimport _, теперь она объявлена явно.go get -u— поднимает зависимости до новых minor и patch версий (major не трогает — это сломало бы путь импорта по SIV).-u=patchограничивает подъём патч-уровнем.go mod tidy— синхронизируетgo.mod/go.sumс реальными импортами кода: добавляет то, что импортируется, но не записано; удаляет то, что записано, но не используется; выравнивает пометки// indirect. Запускать после любых правок импортов — это «привести в порядок».go mod download— просто раскладывает модули по кешу (GOMODCACHE), не собирая проект. Полезно в CI для отдельного слоя кеширования и прогрева.go list -m all— печатает итоговый build list, тот самый результат MVS: какие именно версии всех модулей (прямых и транзитивных) войдут в сборку. Первый инструмент при разборе «откуда взялась эта версия». Рядом полезенgo mod graph(рёбра графа зависимостей) иgo mod why pkg(зачем пакет в сборке).
Отдельно — project-pinned инструменты (Go 1.24+): версия закрепляется в go.mod и воспроизводится вместе с остальными зависимостями, а запуск идёт через тулчейн:
# добавить инструмент: пишет директиву tool И require в go.mod
go get -tool golang.org/x/tools/cmd/stringer@latest
# запустить закреплённой версией
go tool stringer -helptool в go.mod — не просто пометка: рядом go get -tool добавляет обычное require на модуль инструмента, поэтому его версия участвует в том же MVS и go.sum, что и рантайм-зависимости, — и одинакова у всей команды и в CI.
replace и exclude
Две директивы go.mod позволяют вмешаться в выбор версий, когда обычного require мало.
require github.com/some/lib v1.4.0
// 1. локальная разработка: подставить путь на диске
replace github.com/some/lib => ../lib
// 2. форк: перенаправить на свой модуль и версию
replace github.com/some/lib => github.com/khorost/lib v1.4.1-fix
// 3. пиновать проблемную зависимость на конкретную версию
replace golang.org/x/net => golang.org/x/net v0.28.0
// исключить заведомо битую версию из рассмотрения MVS
exclude github.com/broken/dep v1.3.0replace подменяет источник и/или версию модуля:
- Локальная замена (
=> ../lib) — направляет модуль на каталог на диске. Незаменимо при параллельной правке библиотеки и потребителя без публикации промежуточных версий (но для этого сценария сегодня чаще берут workspaces — не пачкаютgo.mod). - Форк (
=> github.com/you/lib v1.2.3) — перенаправляет на другой модуль с явной версией. Так живут с патчем, пока фикс не влит в апстрим. - Пин (
X => X vНиже) — форсирует конкретную версию, обходя MVS-выбор.
Критично помнить: replace действует только в go.mod главного модуля. Когда ваш модуль подключают как зависимость, его replace-директивы игнорируются. Поэтому replace — инструмент для приложения/сервиса, но не способ «за всех» починить транзитивную зависимость в публикуемой библиотеке.
exclude запрещает загрузку конкретной версии. Уместно, когда известная версия битая (сломанный релиз, уязвимость): если граф зависимостей ссылается именно на неё, это требование при построении build list игнорируется. Дальше версию поднимают уже явно — go get/go mod tidy могут добавить в go.mod требование на более высокую (это отличается от старого поведения, когда MVS сам «перескакивал» на следующую доступную; с Go 1.16 исключённое требование просто отбрасывается, а подъём — отдельное явное действие команд). Тоже действует только в главном модуле.
retract: отзыв версий
replace и exclude — инструменты потребителя (действуют в главном модуле). retract решает зеркальную задачу со стороны автора модуля: пометить уже опубликованные версии как отозванные — выпущенные по ошибке, со случайным тегом, с найденной уязвимостью. Физически удалить версию в SIV нельзя: прокси и checksum database неизменяемы, а удаление тега сломало бы всех, кто на него закрепился. retract — способ сказать «эту версию брать не стоит», не нарушая неизменяемость.
retract (
v1.3.0 // выпущено по ошибке — не использовать
[v1.2.0, v1.2.3] // отозвать диапазон версий
)Отозванная версия остаётся скачиваемой — сборки, уже закрепившие её, не ломаются. Но go перестаёт предлагать её при go get pkg@latest и go get -u, а в go list -m -u all помечает как (retracted) с предупреждением. Механика выпуска слегка выворачивает мозг: чтобы отзыв доехал до потребителей, автор добавляет retract в go.mod и тегирует новую, более высокую версию с этой записью — потому что go читает список отзывов из последней доступной версии модуля. Отзыв самой свежей версии, включая ту, что несёт запись, — тоже валиден.
Vendoring
Vendoring — это складывание копий всех зависимостей прямо в репозиторий, в каталог vendor/.
go mod vendor # создаёт ./vendor с исходниками зависимостей
go build ./... # при наличии vendor/ и go>=1.14 сборка идёт из него
go build -mod=mod # явно игнорировать vendor, брать из кеша/сетиgo mod vendor копирует в vendor/ исходники всех модулей из build list и пишет vendor/modules.txt с описью версий. Если vendor/ присутствует и в go.mod указан go 1.14+, команды go по умолчанию собирают из него (-mod=vendor), не обращаясь к кешу и сети. По vendor/modules.txt при этом сверяется согласованность версий с go.mod — что в каталоге лежат ровно те модули и версии, что заявлены. Важно не переоценить: это проверка описи, а не криптографическая целостность содержимого vendor/. go не проверяет, что файлы в каталоге не правили руками; такую подмену ловят пересозданием (go mod vendor заново) и git diff — поэтому vendor/ и коммитят в репозиторий и ревьюят при изменениях, как обычный код.
Когда это оправдано:
- Герметичные и офлайн-сборки — CI без доступа в интернет, окружения с закрытой сетью, воздушный зазор. Всё нужное — в репозитории.
- Устойчивость к исчезновению — код зависимостей не пропадёт, даже если удалят исходный репозиторий или прокси (эту же задачу частично решает прокси).
- Аудит и ревью — изменения зависимостей видны в diff как обычный код.
Минусы:
- Раздувание репозитория и «шумные» diff при обновлении зависимостей.
vendor/надо пересобирать (go mod vendor) после каждой правки зависимостей и держать в синхроне сgo.mod— иначе сборка расходится с манифестом.- Дублирование того, что и так кешируется в
GOMODCACHEи защищаетсяgo.sum+ прокси.
Тренд последних лет — vendoring по умолчанию не нужен: go.sum даёт целостность при загрузке модулей в кеш, а кеширующий прокси — доступность. Тонкость: при сборке из vendor/ (-mod=vendor) хеши go.sum содержимое каталога уже не проверяют — go.sum работает на этапе скачивания в module cache, а не на этапе чтения из vendor/. Поэтому «есть go.sum и есть vendor/» — это две разные гарантии, а не двойная защита одного и того же: первая закрывает загрузку, вторую (неизменность vendor/) обеспечивает уже git-ревью, а не тулчейн. Vendoring остаётся нишевым инструментом для герметичных пайплайнов и жёстких политик поставки.
Workspaces (go.work)
До Go 1.18 разработка нескольких взаимозависимых модулей локально означала засорять go.mod временными replace => ../other, которые легко случайно закоммитить. Workspaces (Go 1.18) решают это чисто: файл go.work объединяет несколько модулей в одно рабочее пространство, не трогая их go.mod.
go work init ./service ./lib # создать go.work с двумя модулями
go work use ./another # добавить ещё один модуль
go work sync # синхронизировать версии зависимостейgo 1.26
use (
./service
./lib
)Пока активен go.work (лежит в текущем каталоге или выше), команды go видят перечисленные в use модули как локальные: service импортирует lib прямо из соседнего каталога, минуя опубликованные версии и без единой строки replace. Импорт lib резолвится в ./lib на диске.
Когда применять:
- Одновременная разработка приложения и его библиотеки (или нескольких библиотек) до публикации версий.
- Мультимодульные монорепозитории, где модули тесно завязаны друг на друга.
- Быстрая проверка правки в зависимости без выпуска промежуточного тега.
go.work — про локальное окружение, а не про распространение: обычно его добавляют в .gitignore (это личная настройка разработчика), тогда как replace в go.mod виден всем и участвует в сборке потребителей. В монорепозитории go.work иногда всё же коммитят — как единую точку сборки. Главное — не путать роли: go.work не заменяет require, он лишь переопределяет, откуда брать перечисленные модули на этой машине.
Публикация своего модуля: теги и свой домен
В Go нет реестра-регистратуры вроде npm или PyPI, куда что-то «пушат». Модуль публикуется самим фактом того, что по его пути доступен VCS-репозиторий с нужным тегом. Отсюда весь процесс релиза сводится к работе с git.
Шаг первый — путь модуля. module-строка в go.mod это и есть адрес, по которому go будет скачивать код. Для публичного модуля на GitHub он совпадает с URL репозитория:
# путь модуля = адрес, по которому его будут импортировать и скачивать
go mod init github.com/khorost/mylib
git init && git add . && git commit -m "init"
git remote add origin git@github.com:khorost/mylib.git
git push -u origin mainШаг второй — версия. Версия модуля — это git-тег формата vMAJOR.MINOR.PATCH. Публикация версии = отправка тега; никакой отдельной команды «залить в реестр» нет:
git tag v1.0.0
git push origin v1.0.0
# теперь любой может: go get github.com/khorost/mylib@v1.0.0Дальше потребитель ставит версию через go get …@v1.0.0 (или @latest — публичный прокси и go увидят новый тег в течение минут). До первого стабильного релиза держат v0.x.y: semver прямо разрешает v0 ломать API между minor, и потребитель этим предупреждён.
Выпуск v2 и выше — единственное место, где мало просто поставить тег. По правилу semantic import versioning major-версия обязана иметь /vN в пути импорта, а значит и в module-строке. Два рабочих способа: подкаталог v2/ со своим go.mod (module github.com/khorost/mylib/v2) либо отдельная мажор-ветка. После этого v2.0.0 тегается и живёт параллельно v1, не конфликтуя — это прямое следствие того, что для Go разные мажоры суть разные модули.
module github.com/khorost/mylib/v2
go 1.26Свой домен вместо github.com/... — это vanity import path (кастомный путь импорта). Идея: адрес импорта указывает на ваш домен, а физически код по-прежнему живёт на GitHub. Тогда импорт не привязан к хостингу — можно сменить GitHub на что угодно, не ломая ни одного import у потребителей. Так устроены реальные vanity-пути вроде golang.org/x/....
Ключевой момент, где легко ошибиться с порядком: префикс пути должен быть настоящим хостом, к которому go обратится по HTTPS. То есть путь начинается с домена ровно так, как он резолвится — khorost.tech/mylib, а не «перевёрнутого» tech.khorost/mylib. Обратный порядок (tech.khorost/...) — это стиль обратного DNS, как имена пакетов в Java: он годится для модулей, которые используют только локально или приватно и никогда не тянут по сети (тогда резолвить путь не требуется), но опубликовать под ним нельзя — хоста tech.khorost не существует, и go get упрётся в DNS.
Механизм — HTML-мета-тег go-import. Когда go разрешает путь khorost.tech/mylib, он запрашивает https://khorost.tech/mylib?go-get=1 и ищет в ответе:
<meta name="go-import"
content="khorost.tech/mylib git https://github.com/khorost-tech/mylib">Три поля: префикс пути модуля, система контроля версий (git), и реальный URL репозитория. Получив это, go идёт клонировать/скачивать уже с GitHub — читатель импортирует khorost.tech/mylib, а код тянется из github.com/khorost-tech/mylib. Отдать такой мета-тег может любой статический ответ по этому URL; на практике настраивают либо крошечный обработчик, либо готовый сервис вроде проекта govanityurls. Версии при этом остаются теми же git-тегами в целевом репозитории — vanity-путь меняет только адрес импорта, не механику версионирования.
Требования к тегам, путям и метаданным
- Тег — валидный semver с префиксом
v.v1.4.2Go увидит как версию;1.4.2безv,release-3и подобное — нет. Пре-релизы (v1.5.0-rc.1) semver допускает, но@latestпредпочитает стабильные версии. - Опубликованная версия неизменяема. Как только тег кто-то скачал через публичный прокси (
proxy.golang.org) и его хеш попал в checksum DB, содержимое версии зафиксировано навсегда. Перетегировать (force-push тега на другой коммит) бесполезно: те, кто уже получил версию, возьмут закешированную, а несовпадение с записью вgo.sumдаст ошибку сборки. Ошиблись в релизе — не «переиздавайте тот же тег», а выпускайте новый патч; вредную версию помечайте директивойretract. - Регистр в пути значим. Пути модулей регистрозависимы, а файловые системы и протокол прокси — нет, поэтому заглавные буквы кодируются восклицательным знаком:
github.com/Azure/azure-sdk-for-goв кеше и запросах прокси выглядит какgithub.com/!azure/.... Практический вывод — по возможности держите путь в нижнем регистре. LICENSE— для витрины, не для сборки. Собрать и импортировать модуль без лицензии можно. Ноpkg.go.devбез распознанного файла лицензии (LICENSE/COPYING) в корне показывает только ограниченную информацию — не рендерит полную документацию и предупреждает о неясных условиях использования. Для публичной библиотеки внятная лицензия в корне — де-факто требование «опубликовано по-человечески».- Приватный модуль публикуется так же — тегом, но мимо публичной инфраструктуры. Тот же
git tag/push. Дальше два независимых факта, которые легко смешать. Первый — про вашу сборку: путь заносят вGOPRIVATE(см. приватные модули), и локальная командаgoперестаёт ходить за ним вproxy.golang.orgи сверять с публичной checksum DB — тянет напрямую из VCS по вашим кредам. Второй — про витрину:pkg.go.devне увидит модуль не из-заGOPRIVATE(это настройка на вашей машине, о которой сервис ничего не знает), а просто потому, что репозиторий приватный и недоступен публичному индексатору. Итог: «публикация» приватного модуля — это доступный репозиторий с тегом внутри вашей сети, аGOPRIVATEлишь настраивает, как ваш тулчейн его достаёт.
Корневой каталог и раскладка
Корень модуля — это каталог, где лежит go.mod. Обычно это корень репозитория. Import-путь любого пакета = путь модуля + относительный путь его каталога от корня: пакет в ./auth/jwt/ модуля github.com/khorost/mylib импортируется как github.com/khorost/mylib/auth/jwt. Каталог = пакет; имя пакета берётся из строк package в его файлах, не из имени каталога.
mylib/
go.mod # ← корень модуля (module github.com/khorost/mylib)
go.sum
LICENSE
mylib.go # пакет mylib — публичный API
auth/jwt/jwt.go # импортируется как .../mylib/auth/jwt
internal/db/db.go # виден только внутри mylib
cmd/server/main.go # бинарник (package main)Три каталога различаются по смыслу — и путать их не стоит:
internal/— единственный каталог со специальным правилом компилятора. Пакеты подinternal/импортируются только из кода, чей корень — родитель этогоinternal/..../mylib/internal/dbдоступен всему внутриmylib, но недоступен любому внешнему модулю. Так прячут детали реализации, не вынося их в публичный API, который потом придётся поддерживать по semver.cmd/— просто соглашение: сюда кладутmain-пакеты, по подкаталогу на бинарник (cmd/server,cmd/migrate). Дляgoэто обычный каталог, но соглашение узнаваемо всем.pkg/— часто встречающийся каталог, у которого нет никакого особого смысла для тулчейна; это лишь стиль раскладки, который одни проекты используют, другие считают лишним слоем. Ключевое отличие отinternal/:pkg/ничего не ограничивает, аinternal/ограничивает импорт по-настоящему.
Несколько мажорных версий: один репозиторий, ветки и теги
Несколько major-версий, как правило, живут в одном репозитории — разными тегами, а физически v2+ отделяется одним из двух способов:
- Мажор-ветка. Ветка
v2в том же репозитории: в еёgo.mod—module .../v2, аv2.x.yтегается на коммит в этой ветке; вmainпродолжает житьv1с тегамиv1.x.y. Важная точность:go/прокси берут код по тегу — тег указывает на конкретный коммит, и после этого ветка уже не важна для загрузки; ветка нужна как линия разработки, где этот коммит появился и куда лягут следующие. - Подкаталог
/v2. В том же дереве заводится каталогv2/со своимgo.mod(module .../v2); теги общие на репозиторий, но для пути.../v2goберёт код из подкаталога. Удобно, когдаv1иv2надо править в одном рабочем дереве.
Практика сопровождения важнее выбора между ними: v1 и v2 — это два параллельно поддерживаемых кодовых потока. Багфикс, актуальный для обоих, приходится бэкпортить и тегать дважды — v1.5.1 в потоке v1 и v2.3.1 в v2. Это и есть настоящая цена мажора: не «поставил тег», а «удвоил поддержку». Поэтому major поднимают только при действительно ломающем изменении публичного API, а не ради косметики — и лишний раз проверяют, нельзя ли обойтись обратно совместимым добавлением в текущем мажоре.
Отдельный исторический случай — суффикс +incompatible. Если старая библиотека доросла до тега v2.0.0 и выше, но без go.mod и без /vN в пути (по правилам домодульной эпохи), go подхватывает такой тег с пометкой вида v2.0.0+incompatible — «это major без SIV-суффикса, берём как есть». В новом коде так не делают: это совместимость со старым миром, а не образец для своих модулей.
Прокси и приватные модули
По умолчанию go качает модули не напрямую из VCS, а через module proxy — proxy.golang.org. Это ускоряет и стабилизирует сборки: прокси кеширует неизменяемые версии, так что удаление или force-push в исходном репозитории уже не ломает тех, кто зависел от тега.
# цепочка прокси; direct — фоллбэк на прямой VCS-доступ
go env -w GOPROXY=https://proxy.golang.org,direct
# приватные модули: не через прокси и не сверять с checksum DB
go env -w GOPRIVATE=git.corp.example.com,github.com/khorost/*
# дефолтные флаги для всех команд go (напр. всегда собирать из vendor)
go env -w GOFLAGS=-mod=vendorGOPROXY— список прокси через запятую; спец-значениеdirectозначает «взять напрямую из VCS»,off— «запретить сеть вовсе». В организации сюда ставят собственный кеширующий прокси (Athens, JFrog, GitLab), чтобы не зависеть от публичного и держать копию всех версий у себя.GOSUMDB— checksum database (по умолчаниюsum.golang.org); проверяет, что скачанная версия совпадает с той, что видел весь мир.offотключает сверку.GOPRIVATE— glob-паттерны путей приватных модулей. Модуль, попавший под паттерн,goне тянет через публичный прокси и не сверяет с публичной checksum DB (внутренний код туда попадать не должен). Это «зонтичная» переменная: она задаёт умолчания сразу дляGONOPROXY(что не гонять через прокси) иGONOSUMDB(что не сверять с sumdb), которые при нужде переопределяются точечно.GOFLAGS— флаги, добавляемые ко всем вызовамgo(например,-mod=vendorили-mod=readonly), чтобы не повторять их руками.
Практический минимум для приватных репозиториев: выставить GOPRIVATE под свой префикс и настроить доступ к VCS (.netrc/SSH/токен) — этого достаточно, чтобы приватные модули качались напрямую, а публичные продолжали идти через прокси с проверкой контрольных сумм.
Что дальше
Управление зависимостями смыкается с остальным тулингом Go — соберите проект, покройте тестами, замерьте:
- Тестирование в Goготовится, с 20 августа —
go test, таблицы, фикстуры; тесты гоняются по тому же build list. - Профилирование и бенчмарки —
go test -bench, pprof; измерять эффект обновления зависимости. - Конфигурация из окружения — env-переменные приложения; идейно рядом с
go env.
Первоисточники — строго официальная документация, все правила SIV/MVS/команд выше сверены по ней:
- Go Modules Reference — исчерпывающий справочник: формат
go.mod/go.sum, semantic import versioning, алгоритм minimal version selection,replace/exclude/retract, vendoring, протокол прокси, приватные модули. - Using Go Modules — серия из официального блога (
using-go-modules,migrating-to-go-modules,publishing-go-modules,v2-go-modules) — практическое введение по шагам. - Go Modules: v2 and Beyond — детальный разбор правила major-версии в пути импорта и выпуска v2+.
- Managing dependencies — рабочие рецепты команд
go get/go mod tidy/go workв официальной документации.
Комментарии