Deep Engineering
Средний·Опубликовано·60 МИН

Модули и зависимости в Go: go.mod, go.sum и минимальная версия

Собеседование по модулям идёт лестницей: что такое модуль — какую версию возьмёт go, если две библиотеки требуют разные — что делают go get и go mod tidy — зачем go.sum — почему импорт /v2 — как подменить зависимость локальной копией — что значит строка go в go.mod. Прогон отвечает на каждый шаг: a просил c v1.1.0, b — v1.3.0, в прокси лежала v1.9.0, а собралась v1.3.0 — и для a тоже; подменённый в прокси архив той же версии go не собрал, напечатав SECURITY ERROR; а replace в go.mod зависимости не сработал вовсе — каталога, на который он указывал, не существовало, и это не стало ошибкой.

Полное техническое изложение

TL;DR

Модуль — это набор пакетов, которые выпускаются под одной версией, а go.mod в его корне — список требований: путь каждой зависимости и наименьшая версия, на которую модуль согласен. Версию, которая попадёт в сборку, go.mod не называет — её вычисляет команда go по всему графу требований. Отдельного lock-файла нет и не нужно: тот же граф при тех же настройках (replace, go.work, флаги -mod) даёт тот же список выбранных версий. Это обещание о версиях, а не об одинаковом бинарнике: на бинарник влияют ещё тулчейн, флаги сборки и платформа.

Из нескольких требований выбирается наибольшее — и ни версией выше. a требует c v1.1.0, b — c v1.3.0, в прокси лежит и v1.9.0. go list -m all показывает example.com/c v1.3.0, и a, просивший v1.1.0, тоже получает v1.3.0: копия у сборки одна на модуль. Новая версия в прокси сборку не меняет — её меняет только новое требование, а пишут его go get и go mod tidy.

Дальше — то, что отличает понимание от пересказа. Строки go.sum бывают двух видов — хеш файлов модуля и хеш его go.mod; сколько их, зависит от графа, и в числе нет правила. Подменённый в прокси архив той же версии go отказывается собирать с SECURITY ERROR. Понижение c до v1.1.0 понижает и b, которому нужна v1.3.0, а когда подходящей версии b нет — выбрасывает его: removed example.com/b. v2 — это другой путь импорта, и c с c/v2 законно живут в одной сборке. replace действует только в главных модулях — в главном модуле, а в рабочем пространстве go.work — в каждом его модуле и в самом go.work; в go.mod обычной зависимости он молча не работает. Заменяют им исходник — и вместе с ним требования из go.mod замены. А строка go 1.21 не даёт собрать for i := range 3 — она задаёт версию языка, а go 1.25.0 при GOTOOLCHAIN=local не даёт собрать модуль компилятором go1.24.7 вовсе.

Порог входа
Перед уроком достаточно понимать
  • что такое пакет и путь импорта — это урок о пакетах;
  • как запустить go build и go run;
  • что версия вида v1.3.0 — это мажорная, минорная и патч-части семантического версионирования: минорная и патч обещают совместимость, мажорная — нет (подробно — в Механизме 4).
Заранее знать не нужно
  • выбор минимальной версии, go get, go mod tidy и // indirect;
  • что лежит в go.sum и что делает go, когда хеш не сходится; импорт /v2;
  • replace, строки go и toolchain, GOTOOLCHAIN.

Что здесь на самом деле спрашивают

Лестница почти всегда такая:

  1. «Что такое модуль и чем он отличается от пакета?» — разминка.
  2. «Две библиотеки требуют разные версии одной зависимости — какую возьмёт go?» — главный вопрос темы, и ответ «последнюю» на нём неверен.
  3. «Что делает go get, что делает go mod tidy и что значит // indirect?» — проверяют, понимает ли кандидат, что go.mod пишут команды, а не только руки.
  4. «Зачем go.sum? Его надо коммитить?» — про проверку целостности.
  5. «Почему у второй мажорной версии в пути /v2?» — про семантический импорт.
  6. «Как подменить зависимость локальной копией?» — replace и его границы.
  7. «Что значит строка go 1.22 в go.mod?» — версия языка и тулчейна.

Стержень у лестницы один: go.mod хранит требования «не ниже такой-то версии», а версию сборки выбирает граф — по требованиям, а не по тому, что лежит в прокси. Почти всё, что спрашивают дальше, из этой фразы и следует.

На чём проверено: прогоны — go1.24.7 и go1.25.1, linux/amd64; цитаты go help, спецификации и исходника сверены 14.10.2026. На эту дату вышли Go 1.26 и 1.27; урок на них не запускался. Тексты сообщений команды go в блоках прогона — вывод этих двух версий, а не контракт: в другой версии формулировки могут отличаться.

База: модуль — дерево пакетов с go.mod в корне

A module is a collection of packages that are released, versioned, and distributed together.

перевод

Модуль — это набор пакетов, которые выпускаются, получают версию и распространяются вместе.

— go help modules
контракт языкаЧто такое модуль, go.mod и go.sum, что делают go get и go mod tidy — документация команды go (go help modules, go.mod, get, mod tidy, module-auth). Прогон bench/gomod/internals.go это иллюстрирует, а не устанавливает.

Пакет — каталог с файлами .go; модуль — дерево таких каталогов с go.mod в корне. Версию получает модуль целиком, а не отдельный пакет. Путь модуля из строки module — префикс путей импорта всех его пакетов.

Самый короткий модуль — один файл и одна команда. Полный файл main.go (он же печатается в разделе 8 прогона); его можно положить в пустой каталог и выполнить две команды — go mod init example.com/hello и go run .:

GO
package main
 
import (
	"fmt"
	"runtime/debug"
)
 
func main() {
	info, _ := debug.ReadBuildInfo()
	fmt.Println("module:", info.Main.Path)
}

Вывод bench/gomod/internals.go, раздел 8:

  $ go mod init example.com/hello
    go: creating new go.mod: module example.com/hello
    go: to add module requirements and sums:
        go mod tidy
  $ go run .
    module: example.com/hello
  go.mod после go mod init:
    module example.com/hello

    go 1.24.7

go mod init записал путь модуля и строку go — версию установленного Go (на go1.25.1 там go 1.25.1). Требований нет: модуль пользуется только стандартной библиотекой. Программа узнала свой путь модуля из информации о сборке, которую go вшивает в бинарник.

Дальше нужны зависимости, а для них — прокси модулей: сервер, который по пути модуля и версии отдаёт её go.mod и архив с исходниками; по умолчанию это proxy.golang.org. Прогон урока строит маленький граф требований — кто какой модуль какой версии требует. Сети у него нет, и она не нужна: прокси модулей — это просто файлы, и документация прямо это разрешает — even a site serving from a fixed file system (including a file:/// URL) (даже сайт, отдающий файлы из неизменной файловой системы, — в том числе адрес file:///) может быть прокси. Скрипт сам кладёт в такой прокси модули example.com/a, b, c и ещё несколько, а потом запускает настоящие команды go со своим кешем модулей. Главный модуль app импортирует a и b, а оба они — c. Вот его go.mod после go mod tidy:

Вывод bench/gomod/internals.go, раздел 1:

  go.mod после go mod tidy:
    module example.com/app

    go 1.24

    require (
        example.com/a v1.0.0
        example.com/b v1.0.0
    )

    require example.com/c v1.3.0 // indirect

Три вида строк. module — путь модуля. go — версия языка и наименьшая версия Go, которой можно собирать модуль (это в «Глубже»). require — требования: путь модуля и наименьшая версия. Первый блок — то, что app импортирует сам; второй, с // indirect, — то, что нужно его зависимостям; оба блока пишет go mod tidy.

Одну таблицу стоит запомнить сразу:

файлчто в нёмкто его пишет
go.modтребования «модуль, не ниже версии»; go, toolchain, replaceвы, go get, go mod tidy
go.sumхеши h1: файлов модуля и его go.mod для модулей сборки (и go.mod части невыбранных версий, если зависимости объявляют go ниже 1.17)go get, go mod tidy
кеш модулейраспакованные исходники, go env GOMODCACHEgo при загрузке

Ответ уровня Junior: модуль — набор пакетов, которые выпускаются под одной версией; go.mod в его корне задаёт путь модуля, версию Go и требования к зависимостям, а go.sum хранит их контрольные суммы. Слово, которое стоит произнести отдельно, — «требования»: в go.mod записана не установленная версия, а нижняя граница.

Механизм 1: выбор минимальной версии — наибольшая из требуемых

Граф прогона: a требует c v1.1.0, b — c v1.3.0, в прокси у c четыре версии. Что выберет go:

Вывод bench/gomod/internals.go, раздел 1:

1) ВЫБОР МИНИМАЛЬНОЙ ВЕРСИИ
───────────────────────────
  граф: app → a v1.0.0 → c v1.1.0;  app → b v1.0.0 → c v1.3.0
  $ go mod tidy
    (вывода нет)
  $ go list -m -versions example.com/c
    example.com/c v1.1.0 v1.3.0 v1.5.0 v1.9.0
  $ go list -m all
    example.com/app
    example.com/a v1.0.0
    example.com/b v1.0.0
    example.com/c v1.3.0
  $ go run .
    a.CVersion() = v1.3.0
    b.CVersion() = v1.3.0

Три вещи в этом выводе:

  • выбрана v1.3.0 — наибольшая из двух требуемых;
  • не v1.9.0, хотя она есть в прокси: её не требует никто;
  • копия c в сборке одна, и a, просивший v1.1.0, работает с v1.3.0. Двух версий одного модуля в одной сборке не бывает (кроме разных мажорных — это Механизм 4).
деталь реализации · Go 1.24Алгоритм — пакет cmd/go/internal/mvs, комментарий к BuildList в строках 83–84 на go1.24.7. Само поведение — наибольшая версия среди требуемых, а не наибольшая доступная — показано прогоном, разделы 1 и 2.

Алгоритм так и называется — выбор минимальной версии (Minimal Version Selection, пакет cmd/go/internal/mvs). Комментарий к функции, которая строит список сборки, говорит, что она traverses the graph and returns a list containing the highest version for each visited module (обходит граф и возвращает список, в котором для каждого посещённого модуля стоит наибольшая версия). «Наибольшая» и «минимальная» здесь не спорят: из посещённых версий берётся наибольшая, и это минимальная версия, которая удовлетворяет всем требованиям. Ни одно требование не просит больше v1.3.0, значит, больше v1.3.0 сборка и не получит.

Сам граф печатает go mod graph:

Вывод bench/gomod/internals.go, раздел 1:

  $ go mod graph
    example.com/app example.com/a@v1.0.0
    example.com/app example.com/b@v1.0.0
    example.com/app example.com/c@v1.3.0
    example.com/app go@1.24
    example.com/a@v1.0.0 example.com/c@v1.1.0
    example.com/a@v1.0.0 go@1.22
    example.com/b@v1.0.0 example.com/c@v1.3.0
    example.com/b@v1.0.0 go@1.22
    example.com/c@v1.3.0 go@1.22
    go@1.24 toolchain@go1.24

Ребро a@v1.0.0 → c@v1.1.0 в графе есть, но в сборку c v1.1.0 не идёт. Третья строка, app → c@v1.3.0, — то самое требование с // indirect, которое записал tidy. Обратите внимание и на рёбра go@…: версия Go — тоже требование в этом графе, и к ней применяется то же правило (это всплывёт в «Глубже»).

Почему так устроено — и почему это хорошо. Выбор версий воспроизводим без lock-файла: тот же набор go.mod при тех же настройках (replace, go.work, флаги -mod) даёт тот же список версий, сегодня и через год, сколько бы новых версий ни вышло. Требование значит «не ниже»: a написан под v1.1.0 и получает v1.3.0 — расчёт на то, что минорные версии обратно совместимы. Если автор c эту совместимость нарушил, сломается a — но при обновлении, которое кто-то сделал явно, а не после чужого релиза.

Механизм 2: go get меняет требования, go mod tidy сверяет их с импортами

Get resolves its command-line arguments to packages at specific module versions, updates go.mod to require those versions, and downloads source code into the module cache.

перевод

Get сопоставляет аргументы командной строки пакетам конкретных версий модулей, обновляет go.mod так, чтобы он требовал эти версии, и загружает исходники в кеш модулей.

— go help get

Ключевое — «обновляет go.mod так, чтобы он требовал». go get не выбирает версию в обход графа: он дописывает требование в go.mod главного модуля, а дальше работает тот же выбор минимальной версии:

Вывод bench/gomod/internals.go, раздел 2:

2) go get: ПОВЫСИТЬ, ПОНИЗИТЬ, -u
─────────────────────────────────
  $ go get example.com/c@v1.5.0
    go: upgraded example.com/c v1.3.0 => v1.5.0
    go.mod: require example.com/c v1.5.0 // indirect
  $ go list -m example.com/c
    example.com/c v1.5.0
  $ go run .
    a.CVersion() = v1.5.0
    b.CVersion() = v1.5.0

Требований к c теперь три — v1.1.0, v1.3.0 и v1.5.0, — и наибольшее выигрывает. Понижение интереснее. b требует c v1.3.0, а другой версии b, которая согласилась бы на v1.1.0, в прокси нет (go.mod перед этим шагом возвращён к исходному, с c v1.3.0):

Вывод bench/gomod/internals.go, раздел 2:

  понижение; других версий b в прокси нет:
  $ go get example.com/c@v1.1.0
    go: removed example.com/b v1.0.0
    go: downgraded example.com/c v1.3.0 => v1.1.0
    go.mod: module example.com/app
    go.mod: require example.com/a v1.0.0
    go.mod: require example.com/c v1.1.0 // indirect
  $ go run .
    main.go:7:2: no required module provides package example.com/b; to add it:
        go get example.com/b
    [код выхода 1]

go get понизил c ровно так, как просили, а противоречие разрешил, выбросив b из требований — с сообщением, но без вопроса. Сборка после этого сломалась. Но удаление — крайний случай. Если в прокси есть версия b, согласная на v1.1.0, та же команда понижает и b:

Вывод bench/gomod/internals.go, раздел 2:

  понижение; в прокси добавлен b v0.9.0, он требует c v1.1.0:
  $ go get example.com/c@v1.1.0
    go: downgraded example.com/b v1.0.0 => v0.9.0
    go: downgraded example.com/c v1.3.0 => v1.1.0
    go.mod: module example.com/app
    go.mod:     example.com/a v1.0.0
    go.mod:     example.com/b v0.9.0
    go.mod: require example.com/c v1.1.0 // indirect
  $ go run .
    a.CVersion() = v1.1.0
    b.CVersion() = v1.1.0

Понижение в модульной системе — не «поставить старую версию», а «понизить всех, кто требует новее»: go get берёт у них версию, согласную на v1.1.0, а если такой нет, как у b в первом случае, — удаляет требование.

Версию выше требуемой дают только явные команды: go get example.com/c без версии или с @latest (по go help get — upgrade it to its latest version (поднять до последней версии)) и go get -u:

Вывод bench/gomod/internals.go, раздел 2:

  $ go get -u ./...
    go: upgraded example.com/c v1.3.0 => v1.9.0
    go.mod: require example.com/c v1.9.0 // indirect
  go get без версии и с @latest:
  $ go get example.com/c
    go: upgraded example.com/c v1.3.0 => v1.9.0
    go.mod: require example.com/c v1.9.0 // indirect
  $ go get example.com/c@latest
    go: upgraded example.com/c v1.3.0 => v1.9.0

-u документация описывает как обновление модулей, дающих зависимости указанных пакетов, to use newer minor or patch releases when available (до более новых минорных или патч-версий, если они есть). Все три — явное действие человека, а не побочный эффект сборки.

go mod tidy делает другое — сверяет go.mod с тем, что код на самом деле импортирует:

It adds any missing modules necessary to build the current module's packages and dependencies, and it removes unused modules that don't provide any relevant packages.

перевод

Он добавляет недостающие модули, нужные для сборки пакетов текущего модуля и их зависимостей, и удаляет неиспользуемые модули, которые не дают ни одного нужного пакета.

— go help mod tidy

Вывод bench/gomod/internals.go, раздел 3:

3) go mod tidy И // indirect
────────────────────────────
  лишнее требование: в go.mod дописан example.com/unused, импорта нет
  $ go mod edit -require=example.com/unused@v1.0.0
    (вывода нет)
  $ go mod tidy -v
    unused example.com/unused
    go.mod: (строк с unused нет)
  импорт без требования: добавлен файл extra.go с import "example.com/unused"
  $ go build -o /dev/null .
    extra.go:3:8: no required module provides package example.com/unused; to add it:
        go get example.com/unused
    [код выхода 1]
  $ go mod tidy
    go: finding module for package example.com/unused
    go: found example.com/unused in example.com/unused v1.0.0
    go.mod:     example.com/unused v1.0.0

Лишнее требование tidy убрал. Обратная ситуация — импорт есть, требования нет — показывает важное: go build по умолчанию go.mod не правит. Он остановился и подсказал команду; дописал требование только tidy. Сборка, которая молча меняет список зависимостей, была бы невоспроизводимой.

«По умолчанию» здесь важно: без каталога vendor go ведёт себя так, будто задан -mod=readonly. Флаг -mod=mod это разрешение снимает — тот же go build сам находит модуль и дописывает требование:

Вывод bench/gomod/internals.go, раздел 9:

9) go build -mod=mod: ПРАВИТЬ go.mod РАЗРЕШЕНО ЯВНО
───────────────────────────────────────────────────
  импорт без требования: добавлен файл extra.go с import "example.com/unused"
  $ go build -o /dev/null .
    extra.go:3:8: no required module provides package example.com/unused; to add it:
        go get example.com/unused
    [код выхода 1]
  $ go build -mod=mod -o /dev/null .
    go: finding module for package example.com/unused
    go: found example.com/unused in example.com/unused v1.0.0
    go.mod:     example.com/unused v1.0.0

Так что правило точнее звучит так: go.mod меняют go get, go mod tidy и сборка с явным -mod=mod (флаг можно задать и через GOFLAGS).

Последнее — что значит // indirect:

Вывод bench/gomod/internals.go, раздел 3:

  прямой импорт c: добавлен файл extra.go с import "example.com/c"
    go.mod до:    require example.com/c v1.3.0 // indirect
  $ go mod tidy
    (вывода нет)
    go.mod после: require example.com/c v1.3.0

// indirect — пометка «ни один пакет главного модуля этот модуль не импортирует; он нужен зависимостям». Стоило app импортировать c самому — пометка ушла. Зачем tidy вообще пишет требования к модулям, которых app не импортирует, — следствие строки go, и это видно на go.sum в следующем механизме.

Механизм 3: go.sum — хеш архива и хеш go.mod

When the go command downloads a module zip file or go.mod file into the module cache, it computes a cryptographic hash and compares it with a known value to verify the file hasn't changed since it was first downloaded. Known hashes are stored in a file in the module root directory named go.sum.

перевод

Когда команда go загружает zip-архив модуля или файл go.mod в кеш модулей, она вычисляет криптографический хеш и сравнивает его с известным значением, чтобы убедиться, что файл не изменился с первой загрузки. Известные хеши хранятся в файле go.sum в корне модуля.

— go help module-auth

Строки go.sum бывают двух видов — хеш файлов модуля и хеш его go.mod. В графе урока вышло по две на каждый модуль сборки:

Вывод bench/gomod/internals.go, раздел 4:

4) go.sum: ХЕШИ И ПОДМЕНА
─────────────────────────
  go.sum после go mod tidy:
    example.com/a v1.0.0 h1:CZz7vqm6MQBHsDphHciMNMa6SlKCJcZr9XrEtYSTg/g=
    example.com/a v1.0.0/go.mod h1:Cg5o19qt63F5qZnxvlazSN42WL9TH81VnebnIKrtmII=
    example.com/b v1.0.0 h1:9aOSoYaMjU23gumsTjqFW+JjjeIti/u9p65uRO8SR94=
    example.com/b v1.0.0/go.mod h1:nPiYYe6I4RA/VudIzX11KxjlL1GUJpo4DFPe0FX/Jvc=
    example.com/c v1.3.0 h1:5VqJIdrvw5IP6QYOEa4QmH5x99fJVd78wLdNFCrrUNc=
    example.com/c v1.3.0/go.mod h1:slkmCTK66XZBf3CTta1WAoTNx6XLaBNrYJ6tHb7WT0g=
  строк в go.sum: 6

Строка без суффикса — хеш всех файлов модуля (их имён и содержимого, а не байтов zip). Префикс h1: — название функции хеширования: base64 от SHA-256 сводки, в которой на каждый файл архива строка «SHA-256 содержимого и имя файла» (golang.org/x/mod/sumdb/dirhash, строки 31–41). Строка с /go.mod — хеш одного только go.mod: чтобы построить граф, go читает go.mod многих версий, а архив качает только для тех, чей код идёт в сборку.

Обратите внимание, чего здесь нет: строки для c v1.1.0, хотя ребро a → c v1.1.0 в графе есть. В go mod graph у c@v1.1.0 не было и своих рёбер: её go.mod go вообще не читал. Причина — строка go 1.22 в go.mod модуля a: с 1.17 go.mod хранит больше требований — потому tidy и записал в app строку c v1.3.0 // indirect, — а граф за такими модулями прорежен: в него входят только их прямые требования (прореживание графа; у главного модуля тоже go 1.17 или выше). Документация tidy говорит об этом скупо: Go versions 1.17 and higher retain more requirements in order to support lazy module loading (Go 1.17 и выше сохраняют больше требований, чтобы поддержать ленивую загрузку модулей). Тот же граф с go 1.16 в go.mod зависимостей:

Вывод bench/gomod/internals.go, раздел 4:

  те же требования, но a и b объявляют в go.mod go 1.16 (olda, oldb):
  $ go mod tidy
    (вывода нет)
    строки go.sum про example.com/c:
      example.com/c v1.1.0/go.mod h1:slkmCTK66XZBf3CTta1WAoTNx6XLaBNrYJ6tHb7WT0g=
      example.com/c v1.3.0 h1:5VqJIdrvw5IP6QYOEa4QmH5x99fJVd78wLdNFCrrUNc=
      example.com/c v1.3.0/go.mod h1:slkmCTK66XZBf3CTta1WAoTNx6XLaBNrYJ6tHb7WT0g=
    строк в go.sum: 7

Теперь go.mod невыбранной v1.1.0 прочитан и захеширован: граф до 1.17 загружается целиком. И попутная деталь: хеши /go.mod у v1.1.0 и v1.3.0 совпали — go.mod у этих версий одинаковый, а хешируется только он.

Отсюда вывод: число строк в go.sum — не инвариант. У модуля может быть только строка /go.mod, у одного модуля — строки нескольких версий, а после команд, которые не чистят файл, — и лишние записи: убирать unnecessary ones (ненужные записи) — работа go mod tidy. Лишняя строка в go.sum — не признак ошибки.

Что go делает с этими хешами. Без go.sum сборка не идёт:

Вывод bench/gomod/internals.go, раздел 4:

  без go.sum:
  $ go build -o /dev/null .
    main.go:6:2: missing go.sum entry for module providing package example.com/a (imported by example.com/app); to add:
        go get example.com/app

А если содержимое версии в прокси подменить — тот же номер v1.3.0, другой код — и собрать модуль на чистом кеше:

Вывод bench/gomod/internals.go, раздел 4:

  в прокси подменено содержимое c v1.3.0 (та же версия, другой код), go.sum прежний:
  $ go build -o /dev/null .
    verifying example.com/c@v1.3.0: checksum mismatch
        downloaded: h1:sT/G26SeVFHyNJDFxu1SduC9eDWsMDygdoosW/dx/bQ=
        go.sum:     h1:5VqJIdrvw5IP6QYOEa4QmH5x99fJVd78wLdNFCrrUNc=
    SECURITY ERROR
    This download does NOT match an earlier download recorded in go.sum.
    The bits may have been replaced on the origin server, or an attacker may
    have intercepted the download attempt.
    For more information, see 'go help module-auth'.
    [код выхода 1]

Сборка остановлена. Ровно так же — checksum mismatch и SECURITY ERROR — go ответил, когда в go.sum испортили один символ хеша, причём и с тёплым кешем модулей, и с пустым. Отсюда практическое правило: go.sum коммитят вместе с go.mod. Он не выбирает версии — это делает go.mod, — он гарантирует, что под номером версии у всех тот же код.

контракт языкаПрогон шёл с GOSUMDB=off: без сети базы контрольных сумм нет. В обычной настройке (go.env дистрибутива: GOSUMDB=sum.golang.org) первая загрузка модуля, которого ещё нет в go.sum, сверяется и с ней — по go help module-auth, хеши могут дополнительно загружаться из базы контрольных сумм — в зависимости от GOSUMDB, GOPRIVATE и GONOSUMDB. Сверка с go.sum, показанная выше, от GOSUMDB не зависит.

Механизм 4: v2 и выше — другой путь импорта

Новая мажорная версия по семантическому версионированию — это право сломать совместимость. Модули доводят это правило до пути: начиная с v2, путь модуля заканчивается на /v2, /v3 и так далее (исключения — пути gopkg.in с суффиксом .vN и версии +incompatible у модулей без go.mod; в прогоне они не проверялись). Исходник проверки пути и версии формулирует правило примером: the path "yaml/v2" only corresponds to semantic versions beginning with "v2." (путь yaml/v2 соответствует только семантическим версиям, начинающимся с v2.) Значит, example.com/c и example.com/c/v2 — разные модули, и в одной сборке они живут законно:

Вывод bench/gomod/internals.go, раздел 5:

5) /v2: ДВЕ МАЖОРНЫЕ ВЕРСИИ В ОДНОЙ СБОРКЕ
──────────────────────────────────────────
  main.go импортирует "example.com/c" и cv2 "example.com/c/v2"
  $ go mod tidy
    go: finding module for package example.com/c/v2
    go: finding module for package example.com/c
    go: found example.com/c in example.com/c v1.9.0
    go: found example.com/c/v2 in example.com/c/v2 v2.0.0
  $ go list -m all
    example.com/two
    example.com/c v1.9.0
    example.com/c/v2 v2.0.0
  $ go run .
    c.Version()   = v1.9.0
    cv2.Version() = v2.0.0
  $ go list -m -versions example.com/c example.com/c/v2
    example.com/c v1.1.0 v1.3.0 v1.5.0 v1.9.0
    example.com/c/v2 v2.0.0

У каждого модуля свой список версий; выбор минимальной версии работает внутри одного пути и мажорные версии не сравнивает вовсе. Отсюда и смысл правила: если бы v2 жила под старым путём, требование c v2.0.0 от одной библиотеки подняло бы до несовместимой версии другую, которой нужна v1. Разные пути делают такую сборку возможной.

И попутно: для нового импорта, которого в графе ещё нет, tidy взял последнюю версию — v1.9.0. Выбор минимальной версии разрешает требования; когда требования нет вовсе, его создают из последней версии в прокси, и с этого момента оно фиксировано в go.mod.

Требование к v2 под путём без /v2 go не принимает уже при разборе go.mod:

Вывод bench/gomod/internals.go, раздел 5:

  v2 под путём без /v2 — требование дописано в go.mod руками:
  $ go mod edit -require=example.com/c@v2.0.0
    (вывода нет)
  $ go build -o /dev/null .
    go: errors parsing go.mod:
    go.mod:6:2: require example.com/c: version "v2.0.0" invalid: should be v0 or v1, not v2
    [код выхода 1]

Для автора библиотеки это значит: выпуск v2 — новый путь в module и во всех внутренних импортах, а пользователи переходят на него, меняя импорты. Это дорого — и так и задумано.

Механизм 5: replace — подмена источника, а не версии

replace говорит: «код этого модуля бери отсюда». Чаще всего — из локального каталога, когда нужно проверить правку в зависимости, не выпуская версию:

Вывод bench/gomod/internals.go, раздел 6:

6) replace: ЛОКАЛЬНЫЙ КАТАЛОГ
─────────────────────────────
  $ go mod edit -replace=example.com/c=../c-local
    (вывода нет)
    go.mod: replace example.com/c => ../c-local
  $ go list -m example.com/c
    example.com/c v1.3.0 => ../c-local
  $ go run .
    a.CVersion() = local copy
    b.CVersion() = local copy
  $ go mod tidy
    (вывода нет)
    строки go.sum про example.com/c после tidy:
      (строк с example.com/c нет)

В этом примере выбранная версия не изменилась — v1.3.0, как и без replace, — но код c для обоих модулей, и a, и b, взят из каталога. Заменяется исходник целиком — а с ним и go.mod замены со своими требованиями (у c-local их нет; случай, где они есть, — ниже). Строк про c в go.sum не осталось: у локального каталога нет версии, которую можно было бы скачать и захешировать.

Главная граница replace — он действует только в главных модулях: в главном модуле, а в рабочем пространстве go.work — в каждом его модуле и в самом go.work. В go.mod обычной зависимости он не действует. В прокси go.mod модуля b выглядит так:

Вывод bench/gomod/internals.go, раздел 6:

  replace в go.mod зависимости: go.mod модуля b из прокси —
    module example.com/b
    go 1.22
    require example.com/c v1.3.0
    replace example.com/c => ../c-fork
  каталога ../c-fork нет нигде, и это не ошибка:
  $ go run .
    a.CVersion() = v1.3.0
    b.CVersion() = v1.3.0

Замена в go.mod зависимости проигнорирована целиком — даже несуществующий каталог ни к чему не привёл.

деталь реализации · Go 1.24cmd/go/internal/modload/modfile.go, функция replacementFrom, строки 347–372 на go1.24.7: замены берутся из go.work и из go.mod главных модулей (цикл по MainModules.Versions() в строке 358). Прогон показывает следствие.

В главном модуле тот же несуществующий каталог — ошибка сборки:

Вывод bench/gomod/internals.go, раздел 6:

  replace в главном модуле на каталог, которого нет:
  $ go mod edit -replace=example.com/c=../c-missing
    (вывода нет)
  $ go build -o /dev/null .
    ../modcache/example.com/a@v1.0.0/a.go:3:8: example.com/c@v1.3.0: replacement directory ../c-missing does not exist
    [код выхода 1]

Рабочее пространство меняет картину. Файл go.work перечисляет каталоги модулей, и по go help environment the modules specified will collectively be used as the main modules (перечисленные в нём модули вместе используются как главные модули). Положим рядом с app копию b — каталог b-dev с тем же replace example.com/c => ../c-local в go.mod — и соберём app через go.work:

Вывод bench/gomod/internals.go, раздел 10:

10) replace: РАБОЧЕЕ ПРОСТРАНСТВО И ТРЕБОВАНИЯ ЗАМЕНЫ
─────────────────────────────────────────────────────
  go.work: use ./app и ./b-dev; b-dev — копия b с replace example.com/c => ../c-local
  $ GOWORK=<tmp>/go.work go list -m example.com/b example.com/c
    example.com/b
    example.com/c v1.3.0 => ./c-local
  $ GOWORK=<tmp>/go.work go run .
    a.CVersion() = local copy
    b.CVersion() = local copy
  без go.work (GOWORK=off) — тот же app:
  $ go run .
    a.CVersion() = v1.3.0
    b.CVersion() = v1.3.0

b стал главным модулем рабочего пространства (версии у него в списке нет), и его replace подействовал на всю сборку. Без go.work та же строка в go.mod b снова ничего не значит.

И второе следствие того, что заменяется исходник целиком: у замены может быть свой go.mod со своими требованиями, и они входят в граф. Каталог c-req объявляет модуль example.com/c и требует example.com/unused:

Вывод bench/gomod/internals.go, раздел 10:

  замена со своими требованиями: go.mod каталога c-req требует example.com/unused
  $ go list -m all
    example.com/app
    example.com/a v1.0.0
    example.com/b v1.0.0
    example.com/c v1.3.0
  $ go mod edit -replace=example.com/c=../c-req
    (вывода нет)
  $ go mod tidy
    (вывода нет)
  $ go list -m all
    example.com/app
    example.com/a v1.0.0
    example.com/b v1.0.0
    example.com/c v1.3.0 => ../c-req
    example.com/unused v1.0.0
  $ go run .
    a.CVersion() = c-req + unused
    b.CVersion() = c-req + unused

Выбранная версия c и здесь v1.3.0, но в списке сборки появился модуль, которого не требовал никто, кроме замены. replace подменяет не «версию на версию», а исходник вместе с его требованиями.

Отсюда два практических вывода. Для автора библиотеки: replace в своём go.mod не исправит сборку пользователей — им придётся написать такой же у себя. Для приложения: replace на ../каталог в закоммиченном go.mod ломает сборку у всех, у кого этого каталога нет, — ровно этой ошибкой, — поэтому его убирают до коммита.

Глубже: строка go — версия языка и наименьший тулчейн

наблюдение замераbench/gomod/internals.go, раздел 7, на go1.24.7 и go1.25.1 linux/amd64. Тулчейны не загружались: прокси прогона — файловый, модуля golang.org/toolchain в нём нет.

У строки go в go.mod две роли, и путать их не стоит.

Первая — версия языка. Спецификация привязывает к версиям языка возможности: оператор range по целому числу помечен Go 1.22. И правило для таких пометок общее — исходник will be rejected if the implied or required language version used by the compiler is older than (будет отвергнут, если подразумеваемая или требуемая версия языка, с которой работает компилятор, старше) нужной версии. Версия языка модуля — это строка go:

Вывод bench/gomod/internals.go, раздел 7:

7) СТРОКИ go И toolchain
────────────────────────
  for i := range 3, в go.mod go 1.21:
  $ go run .
    # example.com/lang
    ./main.go:6:17: cannot range over 3 (untyped int constant): requires go1.22 or later (-lang was set to go1.21; check go.mod)
    [код выхода 1]
  for i := range 3, в go.mod go 1.22:
  $ go run .
    012

Компилятор один — go1.24.7, — а результат решает строка в go.mod. Тем же способом строка go переключает семантику переменной цикла: в модуле с go 1.21 все параллельные подтесты, замкнутые на переменную цикла, видят последний случай таблицы — это разобрано в уроке о тестах.

Вторая роль — наименьшая версия Go, которой модуль можно собирать. Что будет, если она новее установленной, решает GOTOOLCHAIN:

Вывод bench/gomod/internals.go, раздел 7:

  в go.mod go 1.25.0:
  $ GOTOOLCHAIN=local go build -o /dev/null .
    go: go.mod requires go >= 1.25.0 (running go 1.24.7; GOTOOLCHAIN=local)
    [код выхода 1]
  $ GOTOOLCHAIN=auto go build -o /dev/null .
    go: download go1.25.0 for linux/amd64: toolchain not available
    [код выхода 1]

С local go1.24.7 отказался собирать модуль, которому нужен 1.25.0. С auto — это значение по умолчанию из файла go.env дистрибутива, с комментарием Automatically download newer toolchains as directed by go.mod files (автоматически загружать более новые тулчейны, когда этого требуют файлы go.mod), — он попытался скачать go1.25.0 — тулчейн загружается как модуль golang.org/toolchain через тот же GOPROXY (cmd/go/internal/toolchain/select.go, строки 358–372 на go1.24.7), а в файловом прокси прогона его нет. На go1.25.1 обе команды прошли без вывода.

Строка toolchain — уже не требование, а пожелание: какой тулчейн использовать, если выбор за go:

Вывод bench/gomod/internals.go, раздел 7:

  в go.mod go 1.24 и toolchain go1.25.1:
  $ GOTOOLCHAIN=local go env GOVERSION
    go1.24.7
  $ GOTOOLCHAIN=auto go env GOVERSION
    go: download go1.25.1 for linux/amd64: toolchain not available
    [код выхода 1]

С local строка toolchain проигнорирована: модуль требует только go 1.24, и go1.24.7 его собирает. С auto go попытался переключиться на go1.25.1.

И последнее, где видна связь с Механизмом 1. Требование к версии Go распространяется по графу, как требование к модулю: зависимость с go 1.25.0 в своём go.mod поднимает строку go у вас. На go1.24.7 с GOTOOLCHAIN=local go get отказался:

Вывод bench/gomod/internals.go, раздел 7:

  зависимость, чей go.mod требует go 1.25.0:
  $ GOTOOLCHAIN=local go get example.com/fresh@v1.0.0
    go: example.com/fresh@v1.0.0 requires go >= 1.25.0 (running go 1.24.7; GOTOOLCHAIN=local)
    [код выхода 1]

А на go1.25.1 та же команда, ни о чём не спросив, переписала строку go главного модуля:

Вывод того же раздела 7 на go1.25.1 (runs/internals-go1.25.txt):

  $ GOTOOLCHAIN=local go get example.com/fresh@v1.0.0
    go: upgraded go 1.24 => 1.25.0
    go: added example.com/fresh v1.0.0
    go.mod: go 1.25.0

После этого собирать модуль можно только Go 1.25 и новее — у всех, включая CI. Поэтому изменение строки go в ревью смотрят так же внимательно, как новую зависимость.

Как отвечать на собеседовании

Короткий ответ: модуль — набор пакетов, которые выпускаются под одной версией; go.mod в его корне перечисляет требования — модуль и версию «не ниже», — а go.sum хранит хеши архива и go.mod каждого нужного модуля. Версию выбирает алгоритм минимальной версии: для каждого модуля — наибольшая из требуемых в графе, а не последняя в прокси.

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

На «две библиотеки требуют разные версии» — пример с числами. «a требует c v1.1.0, b — v1.3.0, в прокси есть v1.9.0. Выбрана v1.3.0, и a работает с ней же: копия одна. v1.9.0 не попадёт, пока её не потребуют, — например, go get -u».

На «go get против go mod tidy». «go get дописывает или меняет требование и пересчитывает граф; при понижении модули, которым нужна версия выше, тоже понижаются, а если некуда — удаляются. tidy сверяет go.mod с импортами: удаляет лишнее, добавляет недостающее, ставит // indirect. go build по умолчанию go.mod не правит — только с явным -mod=mod».

На «зачем go.sum». «Чтобы под номером версии у всех был тот же код. Строки двух видов — хеш файлов модуля и хеш его go.mod; сколько их, зависит от графа. Подмена содержимого той же версии даёт checksum mismatch и SECURITY ERROR. Коммитится вместе с go.mod».

На «почему /v2». «Мажорная версия — другой модуль с другим путём. Иначе выбор наибольшей версии поднимал бы зависимость через несовместимую границу. c и c/v2 законно живут в одной сборке».

На replace и строку go. «replace подменяет исходник — вместе с требованиями из его go.mod — и действует только в главных модулях: в главном модуле или в модулях рабочего пространства go.work. Строка go — версия языка для этого модуля и наименьшая версия Go для сборки; с GOTOOLCHAIN=auto более новую go попробует скачать».

Дальше спросят

Спросят дальше

Чем go.sum отличается от lock-файла — package-lock.json или poetry.lock?

Короткий ответ

Lock-файл в npm или Poetry фиксирует выбор: без него разрешение диапазонов версий может дать другие версии. go.sum ничего не выбирает — выбор однозначно следует из go.mod всего графа и при тех же настройках воспроизводим и так. go.sum только проверяет, что код под выбранными номерами тот же, что был.

Это видно по прогону: без go.sum go не выбрал «какие-нибудь» версии, а отказался собирать — missing go.sum entry. А в самом go.sum после tidy ровно выбранные версии, и для c — только v1.3.0, если зависимости объявляют go 1.17 и выше; при go 1.16 добавился ещё хеш go.mod невыбранной v1.1.0.

Спросят дальше

Как понизить зависимость, если её требует другая библиотека?

Короткий ответ

Только вместе с этой библиотекой. Требование b к c — v1.3.0 или новее — никуда не девается. go get example.com/c@v1.1.0 сам понизит b, если в прокси есть его версия, которой хватает v1.1.0: в прогоне с b v0.9.0 — go: downgraded example.com/b v1.0.0 => v0.9.0. Если такой версии нет, b удаляется: go: removed example.com/b v1.0.0, и сборка не собирается.

Если библиотеку понижать нельзя, остаётся подменить код c через replace (Механизм 5): требование b к v1.3.0 останется, а код придёт откуда скажете.

Спросят дальше

Почему go build не добавляет недостающую зависимость сам?

Короткий ответ

Потому что сборка не должна менять список зависимостей. В прогоне go build на импорте без требования остановился: no required module provides package example.com/unused; to add it — и подсказал go get. Требование дописал только go mod tidy.

Если бы go build молча дописывал зависимости, одна и та же команда в CI и на ноутбуке могла бы собрать разное. Без каталога vendor, по go help build, the go command acts as if -mod=readonly were set (команда go действует так, будто задан -mod=readonly): go.mod и go.sum меняют только команды, которые для этого и существуют. Это поведение по умолчанию, а не запрет: с go build -mod=mod прогон нашёл модуль и дописал требование сам (раздел 9).

Спросят дальше

Как разработать два модуля одновременно, не выпуская версий?

Короткий ответ

Временный replace на локальный каталог — то, что показано в Механизме 5: go list -m печатает v1.3.0 => ../c-local, и оба зависимых модуля берут код из каталога. Его убирают перед коммитом: каталога у других нет.

Для того же случая есть файл go.work — рабочее пространство. По go help environment, если он найден, the modules specified will collectively be used as the main modules (перечисленные в нём модули вместе используются как главные модули) — а значит, их replace и их код действуют все сразу. В прогоне так и вышло: b-dev в рабочем пространстве заменил b, а его replace направил c в ../c-local (Механизм 5).

Частые заблуждения

Утверждение

go берёт последнюю версию зависимости

На самом деле

Для зависимости, которая уже есть в графе, — наибольшую из требуемых. В прогоне в прокси лежала v1.9.0, а выбрана v1.3.0: её требует b, а v1.9.0 не требует никто. Последнюю версию берут только явные команды — go get модуль или @latest, go get -u — и tidy для нового импорта без требования.

Утверждение

У каждой библиотеки своя копия зависимости нужной ей версии

На самом деле

Копия одна на модуль (на мажорную версию). a требовал c v1.1.0 и получил v1.3.0 — ту же, что b: a.CVersion() = v1.3.0.

Утверждение

В go.mod записаны версии, которые стоят в проекте

На самом деле

В go.mod записаны нижние границы. Какая версия попадёт в сборку, показывает go list -m all: требование a к c v1.1.0 в графе есть, а в сборке — v1.3.0.

Утверждение

go.sum — это lock-файл, и его можно не коммитить

На самом деле

Версии он не выбирает, но без него сборка не идёт: missing go.sum entry for module providing package. И именно он ловит подмену: архив той же версии с другим кодом дал checksum mismatch и SECURITY ERROR.

Утверждение

go build сам скачает и допишет недостающую зависимость

На самом деле

По умолчанию нет: no required module provides package example.com/unused; to add it. Дописывают go get и go mod tidy — или сама сборка, если явно задан -mod=mod: тогда go build нашёл модуль и дописал требование.

Утверждение

Две мажорные версии одной библиотеки в одной сборке — конфликт

На самом деле

example.com/c и example.com/c/v2 — разные модули с разными путями. В прогоне одна программа напечатала и v1.9.0, и v2.0.0.

Утверждение

replace в go.mod библиотеки исправит зависимость у всех её пользователей

На самом деле

replace действует только в главных модулях — в главном модуле или в модулях рабочего пространства go.work. В go.mod модуля b как обычной зависимости стоял replace на несуществующий каталог — и сборка app его не заметила.

Утверждение

Строка go в go.mod — просто для информации

На самом деле

Это версия языка модуля и наименьшая версия Go для сборки. С go 1.21 for i := range 3 не собрался — requires go1.22 or later; с go 1.25.0 и GOTOOLCHAIN=local go1.24.7 отказался собирать модуль вовсе.

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.

Практика · что напечатает

Приложение требует x v1.0.0 и y v1.0.0. x требует z v1.2.0 (и w v1.0.0), y — z v1.4.0; в прокси у z есть v1.2.0, v1.4.0 и v1.7.0. После go mod tidy запускают программу и go list -m example.com/z. Код ниже — фрагменты go.mod и main.go; полный скрипт, который строит этот граф и печатает ответ, — bench/gomod/practice.go. Что будет напечатано?
// go.mod модуля x
module example.com/x
go 1.22
require (
example.com/z v1.2.0
example.com/w v1.0.0
)

// go.mod модуля y
module example.com/y
go 1.22
require example.com/z v1.4.0

// в прокси у z есть v1.2.0, v1.4.0 и v1.7.0

// main.go приложения
fmt.Println("x:", x.ZVersion())
fmt.Println("y:", y.ZVersion())

Практика · оцените

Тот же граф: приложение → x и y; x требует z v1.2.0 и w v1.0.0, y — z v1.4.0; все go.mod зависимостей объявляют go 1.22. Сколько строк окажется в go.sum после go mod tidy?
строк

Проверка знаний

Вопрос 1 из 5

a требует c v1.1.0, b требует c v1.3.0, в прокси есть c v1.9.0. Какая версия c попадёт в сборку приложения, которое требует a и b?

Источники и что читать дальше

12 ИСТОЧНИКОВ

  1. Команда go — go help modules и go help go.modОфициальная документация. Что такое модуль: «A module is a collection of packages that are released, versioned, and distributed together» (Модуль — это набор пакетов, которые выпускаются, получают версию и распространяются вместе). Где его граница: «A module version is defined by a tree of source files, with a go.mod file in its root» (Версия модуля задаётся деревом исходных файлов с файлом go.mod в корне).https://pkg.go.dev/cmd/go#hdr-Modules__module_versions__and_more
  2. Команда go — go help get и go help mod tidyОфициальная документация. Что делает go get: «Get resolves its command-line arguments to packages at specific module versions, updates go.mod to require those versions, and downloads source code into the module cache» (Get сопоставляет аргументы командной строки пакетам конкретных версий модулей, обновляет go.mod так, чтобы он требовал эти версии, и загружает исходники в кеш модулей). Что делает tidy: «It adds any missing modules necessary to build the current module's packages and dependencies, and it removes unused modules that don't provide any relevant packages. It also adds any missing entries to go.sum and removes any unnecessary ones» (Он добавляет недостающие модули, нужные для сборки пакетов текущего модуля и их зависимостей, и удаляет неиспользуемые модули, которые не дают ни одного нужного пакета. Он также добавляет недостающие записи в go.sum и удаляет ненужные). И про версию в строке go: «Go versions 1.17 and higher retain more requirements in order to support lazy module loading» (Go 1.17 и выше сохраняют больше требований, чтобы поддержать ленивую загрузку модулей).https://pkg.go.dev/cmd/go#hdr-Add_missing_and_remove_unused_modules
  3. Команда go — go help module-authОфициальная документация. Зачем go.sum: «When the go command downloads a module zip file or go.mod file into the module cache, it computes a cryptographic hash and compares it with a known value to verify the file hasn't changed since it was first downloaded. Known hashes are stored in a file in the module root directory named go.sum» (Когда команда go загружает zip-архив модуля или файл go.mod в кеш модулей, она вычисляет криптографический хеш и сравнивает его с известным значением, чтобы убедиться, что файл не изменился с первой загрузки. Известные хеши хранятся в файле go.sum в корне модуля).https://pkg.go.dev/cmd/go#hdr-Module_authentication_using_go_sum
  4. Команда go — go help goproxyОфициальная документация. Почему прогон урока обходится без сети: «The requests have no query parameters, so even a site serving from a fixed file system (including a file:/// URL) can be a module proxy» (У запросов нет параметров, поэтому прокси модулей может быть даже сайт, отдающий файлы из неизменной файловой системы, — в том числе адрес file:///).https://pkg.go.dev/cmd/go#hdr-Module_proxy_protocol
  5. cmd/go/internal/mvs/mvs.go — BuildListИсходный код Go. Строки 83–84 на go1.24.7, комментарий к функции, которая строит список сборки: «BuildList traverses the graph and returns a list containing the highest version for each visited module» (BuildList обходит граф и возвращает список, в котором для каждого посещённого модуля стоит наибольшая версия). Пакет так и называется: «Package mvs implements Minimal Version Selection» (Пакет mvs реализует выбор минимальной версии), строка 5.https://github.com/golang/go/blob/go1.24.7/src/cmd/go/internal/mvs/mvs.go#L74-L91
  6. golang.org/x/mod/sumdb/dirhash/hash.go — Hash1Исходный код Go. Строки 31–41 (копия в src/cmd/vendor на go1.24.7) — что такое префикс h1: в go.sum: «Hash1 is "h1:" followed by the base64-encoded SHA-256 hash of a summary prepared as if by the Unix command: sha256sum $(find . -type f | sort) | sha256sum» (Hash1 — это h1:, за которым следует base64 от SHA-256 сводки, подготовленной как будто Unix-командой sha256sum $(find . -type f | sort) | sha256sum).https://github.com/golang/go/blob/go1.24.7/src/cmd/vendor/golang.org/x/mod/sumdb/dirhash/hash.go#L31-L41
  7. Команда go — go help build, флаг -modОфициальная документация. Почему go build не правит go.mod: «Otherwise, the go command acts as if -mod=readonly were set» (В остальных случаях команда go действует так, будто задан -mod=readonly) — «в остальных» значит «если нет каталога vendor».https://pkg.go.dev/cmd/go#hdr-Compile_packages_and_dependencies
  8. Файл go.env дистрибутива go1.24.7Официальная документация. Значения по умолчанию: GOPROXY=https://proxy.golang.org,direct, GOSUMDB=sum.golang.org и GOTOOLCHAIN=auto с комментарием «Automatically download newer toolchains as directed by go.mod files» (Автоматически загружать более новые тулчейны, когда этого требуют файлы go.mod).https://github.com/golang/go/blob/go1.24.7/go.env
  9. golang.org/x/mod/module/module.go — CheckИсходный код Go. Строки 210–215 (копия в src/cmd/vendor на go1.24.7): путь и версия модуля обязаны соответствовать друг другу: «For example, the path "yaml/v2" only corresponds to semantic versions beginning with "v2."» (Например, путь yaml/v2 соответствует только семантическим версиям, начинающимся с v2.).https://github.com/golang/go/blob/go1.24.7/src/cmd/vendor/golang.org/x/mod/module/module.go#L210-L215
  10. cmd/go/internal/modload/modfile.go — replacementFromИсходный код Go. Строки 347–372 на go1.24.7: замены ищутся в файле go.work и в индексах главных модулей — цикл for _, v := range MainModules.Versions() в строке 358. go.mod зависимостей здесь не читается вовсе; прогон показывает следствие: replace в go.mod модуля b не сработал.https://github.com/golang/go/blob/go1.24.7/src/cmd/go/internal/modload/modfile.go#L347-L372
  11. cmd/go/internal/toolchain/select.go — загрузка тулчейнаИсходный код Go. Строки 346 и 358–372 на go1.24.7: «GOTOOLCHAIN=auto looks in PATH and then falls back to download» (GOTOOLCHAIN=auto ищет в PATH, а если не нашёл — загружает); тулчейн загружается как модуль golang.org/toolchain версии v0.0.1-<тулчейн>.<GOOS>-<GOARCH> (константы в строках 46–47) — через тот же GOPROXY. Если модуля нет, ошибка строки 372: toolchain not available.https://github.com/golang/go/blob/go1.24.7/src/cmd/go/internal/toolchain/select.go#L346-L375
  12. Спецификация Go — Language versionsОфициальная документация. Что значит версия языка: «Source code containing an integer literal such as 0b1011 will be rejected if the implied or required language version used by the compiler is older than Go 1.13» (Исходник с целочисленным литералом вроде 0b1011 будет отвергнут, если подразумеваемая или требуемая версия языка, с которой работает компилятор, старше Go 1.13). И пункт для Go 1.22: «A "for" statement with "range" clause may iterate over integer values from zero to an upper limit» (Оператор for с range может перебирать целые числа от нуля до верхней границы).https://go.dev/ref/spec#Language_versions