MEASUREMENT
bench/gomod/internals.go
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/interview/golang/modules
- How to run it
go run bench/gomod/internals.go /usr/local/go1.25.1/bin/go run bench/gomod/internals.go go run bench/gomod/practice.go
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
Замеры для урока «Модули и зависимости»
| Файл | Что делает |
|---|---|
internals.go |
десять наблюдений, каждое — запуск настоящей команды go на маленьком графе модулей: выбор минимальной версии (go list -m all, go mod graph), go get вверх (явной версией, -u, без версии и @latest), вниз (без подходящей версии b и с ней), go mod tidy и // indirect, содержимое go.sum и подмена хеша и содержимого версии, две мажорные версии c и c/v2 в одной сборке, replace в главном модуле и в go.mod зависимости, строки go и toolchain при GOTOOLCHAIN=local и auto; после аудита 14.10.2026 — разделы 8–10 в конце: минимальный модуль (go mod init, go run), go build -mod=mod, replace в рабочем пространстве go.work и замена со своими требованиями |
practice.go |
источник ответов двух практических задач урока: какую версию z увидят x и y и сколько строк окажется в go.sum |
go run bench/gomod/internals.go
/usr/local/go1.25.1/bin/go run bench/gomod/internals.go
go run bench/gomod/practice.go
Сети не нужно. Оба скрипта сами собирают прокси модулей в файловой
системе — протокол GOPROXY это прямо разрешает (go help goproxy: прокси
может отдавать файлы и по адресу file:///). Раскладка — <модуль>/@v/list,
<версия>.info, <версия>.mod, <версия>.zip; zip собирается
archive/zip, все файлы в нём лежат под префиксом <модуль>@<версия>/.
Модули example.com/{a,b,c,c/v2,olda,oldb,unused,fresh} и
example.com/{x,y,z,w} выдуманы, их исходники — строки внутри скриптов.
Окружение изолировано. Свои GOMODCACHE, GOCACHE и GOPATH во
временном каталоге, GOPROXY=file://…, GOSUMDB=off (без сети базы
контрольных сумм нет; go.sum проверяется как обычно), GOFLAGS пуст —
поведение по умолчанию, -mod=readonly, — GOENV=off, GOWORK=off,
GOTOOLCHAIN=local везде, кроме раздела 7, где GOTOOLCHAIN — предмет
наблюдения. Общие кеши не трогаются; в конце скрипт делает
go clean -modcache своего кеша и удаляет временный каталог.
Вывод детерминирован: хеши h1: зависят только от имён и содержимого файлов
модуля. Временные пути заменены плейсхолдерами <tmp> и <proxy>,
табуляция — четырьмя пробелами; строки go: downloading … опущены, а подряд
идущие go: finding … и go: found … отсортированы — их порядок зависит от
параллельной загрузки.
Записи прогонов — в runs/: internals.txt и practice.txt — go1.24.7
linux/amd64, internals-go1.25.txt — go1.25.1. Прогоны совпали строка в
строку везде, кроме раздела 7 и строки go в go.mod после go mod init
(раздел 8: go 1.24.7 и go 1.25.1). Времени ничего не замеряет; прогон
internals.go занимает около 15 секунд, почти всё — холодная сборка fmt в
пустом GOCACHE.
Что получилось
Выбрана наибольшая из требуемых, а не последняя. a требует
c v1.1.0, b — c v1.3.0, в прокси у c есть v1.1.0 v1.3.0 v1.5.0 v1.9.0. go list -m all даёт example.com/c v1.3.0, и a.CVersion()
возвращает v1.3.0 — копия c в сборке одна.
go get пишет требование, а не выбирает версию. go get c@v1.5.0 —
upgraded example.com/c v1.3.0 => v1.5.0, и оба модуля видят v1.5.0.
go get c@v1.1.0 понизил c и, раз другой версии b в прокси нет, удалил
b из требований: removed example.com/b v1.0.0, после чего сборка не
собралась. Когда в прокси добавлен b v0.9.0 с требованием c v1.1.0, та же
команда понизила и его: downgraded example.com/b v1.0.0 => v0.9.0, оба
модуля видят v1.1.0. Выше требуемой версию подняли только явные команды:
go get -u ./..., go get example.com/c и go get example.com/c@latest —
каждая v1.3.0 => v1.9.0.
go mod tidy сверяет go.mod с импортами, go build — нет. Лишнее
требование tidy -v удалил (unused example.com/unused); импорт без
требования go build не собрал (no required module provides package), а
tidy дописал. Пометка // indirect ушла, как только app импортировал c
сам.
go.sum — в этом графе по две строки на модуль сборки; число строк — не правило. После tidy в нём 6 строк:
хеш архива и хеш go.mod для a, b, c v1.3.0. Для невыбранной
c v1.1.0 строки нет — зависимости объявляют go 1.22, и её go.mod не
читался. С go 1.16 у зависимостей строк 7: добавилась
example.com/c v1.1.0/go.mod. Её хеш совпал с хешем go.mod у v1.3.0 —
файлы одинаковые. Без go.sum — missing go.sum entry; с испорченным хешем
и с подменённым в прокси содержимым той же версии — checksum mismatch и
SECURITY ERROR, и с тёплым кешем модулей, и с пустым.
c и c/v2 — разные модули. Одна программа напечатала v1.9.0 и
v2.0.0. Требование example.com/c v2.0.0 отвергнуто при разборе go.mod:
should be v0 or v1, not v2.
replace — только в главном модуле. С replace example.com/c => ../c-local go list -m показал v1.3.0 => ../c-local, оба модуля взяли код
из каталога (local copy), строк про c в go.sum не осталось. Несуществующий каталог в
replace главного модуля — ошибка replacement directory ../c-missing does not exist; тот же несуществующий каталог в go.mod модуля b — не замечен
вовсе.
Строка go — версия языка и наименьший тулчейн. С go 1.21
for i := range 3 не собрался (requires go1.22 or later), с go 1.22
напечатал 012. На go1.24.7 модуль с go 1.25.0 при GOTOOLCHAIN=local не
собрался (go.mod requires go >= 1.25.0), при auto — попытка скачать
тулчейн (toolchain not available: в файловом прокси его нет); строка
toolchain go1.25.1 при local проигнорирована. Зависимость с go 1.25.0:
на go1.24.7 go get отказал, на go1.25.1 — upgraded go 1.24 => 1.25.0.
Практика. x требует z v1.2.0, y — z v1.4.0, в прокси есть
v1.7.0: оба видят v1.4.0. В go.sum 8 строк — по две на x, y, w и
z v1.4.0.
Минимальный модуль (раздел 8). go mod init example.com/hello в пустом
каталоге с одним main.go на стандартной библиотеке, go run . печатает
module: example.com/hello; в go.mod — путь и go 1.24.7 (на go1.25.1 —
go 1.25.1), требований нет.
-mod=mod (раздел 9). Тот же импорт без требования: go build без флагов
отказывает, go build -mod=mod находит example.com/unused v1.0.0 и
дописывает требование в go.mod.
replace в рабочем пространстве и требования замены (раздел 10).
go.work с use ./app ./b-dev, где b-dev — копия b с
replace example.com/c => ../c-local: go list -m показывает
example.com/c v1.3.0 => ./c-local, оба модуля печатают local copy; без
go.work — снова v1.3.0. Замена c => ../c-req, чей go.mod требует
example.com/unused: после tidy в go list -m all появился
example.com/unused v1.0.0, программа печатает c-req + unused.
Script
617 lines//go:build ignore
// Модули и зависимости: что на самом деле делают go.mod, go.sum, go get,
// go mod tidy, replace и строка go.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Вопросы о модулях на собеседовании звучат как
// справочные — «что лежит в go.sum», «какую версию выберет go». Ответ,
// который отличает знающего, — о поведении: из двух требований выбирается
// большее, а не последняя версия в прокси; go.sum хранит хеш даже тех
// версий, что в сборку не попали; replace в чужом go.mod не действует.
// Здесь каждое такое поведение получено запуском команды go, а не
// пересказано.
//
// СЕТИ НЕТ, И ОНА НЕ НУЖНА. Программа сама собирает прокси модулей в
// файловой системе: протокол GOPROXY — это просто файлы
// <модуль>/@v/list, <версия>.info, .mod и .zip, и `go help goproxy`
// прямо разрешает file:///. Модули example.com/{a,b,c,c/v2,olda,oldb,unused,fresh}
// — выдуманные и маленькие; их исходники записаны ниже строками.
//
// ГРАФ, НА КОТОРОМ СТОИТ УРОК:
//
// app → a v1.0.0 → c v1.1.0
// app → b v1.0.0 → c v1.3.0
// в прокси у c есть v1.1.0, v1.3.0, v1.5.0, v1.9.0 и отдельный модуль c/v2
//
// ДЕСЯТЬ СЮЖЕТОВ:
//
// 1. Минимальная версия: выбрана v1.3.0, хотя в прокси есть v1.9.0.
// 2. go get: повышение, понижение (с подходящей версией b и без неё),
// -u, без версии и @latest.
// 3. go mod tidy: лишнее требование, // indirect, go build не правит go.mod.
// 4. go.sum: две строки на модуль, подмена хеша и подмена содержимого.
// 5. /v2: две мажорные версии в одной сборке.
// 6. replace на локальный каталог; replace в go.mod зависимости.
// 7. Строки go и toolchain: версия языка и минимальный тулчейн.
// 8. Минимальный модуль: go mod init и go run (полный пример).
// 9. go build -mod=mod правит go.mod сам.
// 10. replace в рабочем пространстве go.work и замена со своими требованиями.
//
// ОКРУЖЕНИЕ. Всё во временном каталоге: свои GOMODCACHE, GOCACHE, GOPATH,
// GOPROXY=file://…, GOSUMDB=off (базы контрольных сумм без сети нет; go.sum
// при этом проверяется как обычно), GOFLAGS пуст (поведение по умолчанию,
// -mod=readonly), GOENV=off, GOWORK=off, GOTOOLCHAIN=local — кроме мест, где
// сам GOTOOLCHAIN предмет наблюдения. Кеши пользователя не трогаются.
//
// В выводе временные пути заменены плейсхолдерами <tmp> и <proxy>, табуляция
// — четырьмя пробелами; строки «go: downloading …» опущены, а подряд идущие
// «go: finding …» и «go: found …» отсортированы: их порядок зависит от
// параллельной загрузки.
//
// ЗАПУСК:
//
// go run bench/gomod/internals.go
// /usr/local/go1.25.1/bin/go run bench/gomod/internals.go
package main
import (
"archive/zip"
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"strings"
)
var (
work string // временный каталог программы
proxy string // <work>/proxy — корень файлового прокси
goBin string // go той же версии, что запустила программу
baseEnv []string
)
type result struct {
out string
code int
}
// run запускает go в каталоге dir с добавочными переменными окружения.
func run(dir string, env []string, args ...string) result {
cmd := exec.Command(goBin, args...)
cmd.Dir = dir
cmd.Env = append(append([]string{}, baseEnv...), env...)
out, err := cmd.CombinedOutput()
code := 0
if ee, ok := err.(*exec.ExitError); ok {
code = ee.ExitCode()
} else if err != nil {
panic(err)
}
return result{string(out), code}
}
func clean(s string) string {
s = strings.ReplaceAll(s, "file://"+proxy, "<proxy>")
s = strings.ReplaceAll(s, proxy, "<proxy>")
s = strings.ReplaceAll(s, work, "<tmp>")
s = strings.ReplaceAll(s, "\t", " ")
return s
}
// lines печатает вывод команды с отступом, без пустых строк и без
// «go: downloading».
func lines(out, indent string) {
printed := false
for _, l := range sortFinding(strings.Split(strings.TrimRight(out, "\n"), "\n")) {
if strings.TrimSpace(l) == "" || strings.HasPrefix(l, "go: downloading ") {
continue
}
fmt.Println(indent + strings.TrimRight(clean(l), " "))
printed = true
}
if !printed {
fmt.Println(indent + "(вывода нет)")
}
}
// sortFinding сортирует подряд идущие строки «go: finding module for
// package …» и «go: found …»: tidy ищет пакеты параллельно, и их порядок от
// запуска к запуску разный.
func sortFinding(ls []string) []string {
out := append([]string(nil), ls...)
for _, prefix := range []string{"go: finding module for package ", "go: found "} {
for i := 0; i < len(out); {
j := i
for j < len(out) && strings.HasPrefix(out[j], prefix) {
j++
}
if j > i {
sort.Strings(out[i:j])
i = j
} else {
i++
}
}
}
return out
}
// cmd печатает команду, её вывод и — если не ноль — код выхода.
func cmd(dir string, env []string, args ...string) result {
shown := "go " + strings.Join(args, " ")
for _, e := range env {
shown = e + " " + shown
}
fmt.Println(" $ " + clean(shown))
r := run(dir, env, args...)
lines(r.out, " ")
if r.code != 0 {
fmt.Printf(" [код выхода %d]\n", r.code)
}
return r
}
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
func write(path, body string) {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
panic(err)
}
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
panic(err)
}
}
func read(path string) string {
b, err := os.ReadFile(path)
if err != nil {
panic(err)
}
return string(b)
}
// file печатает файл целиком с отступом.
func file(label, path string) {
fmt.Println(" " + label + ":")
for _, l := range strings.Split(strings.TrimRight(read(path), "\n"), "\n") {
if l == "" {
fmt.Println()
continue
}
fmt.Println(" " + clean(l))
}
}
// publish кладёт версию модуля в файловый прокси: .mod, .info, .zip и
// строку в list. Раскладка — из протокола GOPROXY: <модуль>/@v/<версия>.*;
// в zip все файлы лежат под префиксом <модуль>@<версия>/.
func publish(mod, ver string, files map[string]string) {
dir := filepath.Join(proxy, mod, "@v")
write(filepath.Join(dir, ver+".mod"), files["go.mod"])
write(filepath.Join(dir, ver+".info"), fmt.Sprintf(`{"Version":%q,"Time":"2026-01-01T00:00:00Z"}`+"\n", ver))
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
names := make([]string, 0, len(files))
for n := range files {
names = append(names, n)
}
sort.Strings(names)
for _, n := range names {
w, err := zw.Create(mod + "@" + ver + "/" + n)
if err != nil {
panic(err)
}
w.Write([]byte(files[n]))
}
if err := zw.Close(); err != nil {
panic(err)
}
write(filepath.Join(dir, ver+".zip"), buf.String())
list := filepath.Join(dir, "list")
old, _ := os.ReadFile(list)
if !strings.Contains(string(old), ver+"\n") {
write(list, string(old)+ver+"\n")
}
}
// cFiles — модуль example.com/c версии ver: функция Version сообщает, из
// какой версии собран пакет.
func cFiles(path, ver, goLine string) map[string]string {
return map[string]string{
"go.mod": "module " + path + "\n\ngo " + goLine + "\n",
"c.go": "package c\n\nfunc Version() string { return \"" + ver + "\" }\n",
}
}
// user — модуль, который зависит от c: a или b.
func user(name, goLine, cver, extra string) map[string]string {
return map[string]string{
"go.mod": "module example.com/" + name + "\n\ngo " + goLine + "\n\nrequire example.com/c " + cver + "\n" + extra,
name + ".go": "package " + name + "\n\nimport \"example.com/c\"\n\n" +
"func CVersion() string { return c.Version() }\n",
}
}
const appMod = `module example.com/app
go 1.24
require (
example.com/a v1.0.0
example.com/b v1.0.0
)
`
const appMain = `package main
import (
"fmt"
"example.com/a"
"example.com/b"
)
func main() {
fmt.Println("a.CVersion() =", a.CVersion())
fmt.Println("b.CVersion() =", b.CVersion())
}
`
// snapshot и restore сохраняют go.mod и go.sum модуля app между сюжетами.
type snap struct{ mod, sum string }
func snapshot(dir string) snap {
sum, _ := os.ReadFile(filepath.Join(dir, "go.sum"))
return snap{read(filepath.Join(dir, "go.mod")), string(sum)}
}
func restore(dir string, s snap) {
write(filepath.Join(dir, "go.mod"), s.mod)
write(filepath.Join(dir, "go.sum"), s.sum)
}
// cleanCache очищает кеш модулей: следующая команда скачает всё заново.
func cleanCache() {
if r := run(work, nil, "clean", "-modcache"); r.code != 0 {
panic(r.out)
}
}
// grep печатает строки файла, содержащие needle, или «(строк нет)».
func grep(path, needle, indent string) {
found := false
for _, l := range strings.Split(read(path), "\n") {
if strings.Contains(l, needle) {
fmt.Println(indent + clean(l))
found = true
}
}
if !found {
fmt.Println(indent + "(строк с " + strings.TrimSpace(needle) + " нет)")
}
}
func main() {
goBin = filepath.Join(runtime.GOROOT(), "bin", "go")
if _, err := os.Stat(goBin); err != nil {
goBin = "go"
}
var err error
work, err = os.MkdirTemp("", "gomod")
if err != nil {
panic(err)
}
proxy = filepath.Join(work, "proxy")
defer func() {
run(work, nil, "clean", "-modcache")
os.RemoveAll(work)
}()
var env []string
for _, e := range os.Environ() {
if !strings.HasPrefix(e, "GO") {
env = append(env, e)
}
}
baseEnv = append(env,
"GOPROXY=file://"+proxy,
"GOSUMDB=off",
"GOFLAGS=",
"GOENV=off",
"GOWORK=off",
"GOTOOLCHAIN=local",
"GOPATH="+filepath.Join(work, "gopath"),
"GOMODCACHE="+filepath.Join(work, "modcache"),
"GOCACHE="+filepath.Join(work, "gocache"),
)
// Прокси: c в четырёх версиях, a и b, c/v2, unused и fresh.
for _, v := range []string{"v1.1.0", "v1.3.0", "v1.5.0", "v1.9.0"} {
publish("example.com/c", v, cFiles("example.com/c", v, "1.22"))
}
publish("example.com/a", "v1.0.0", user("a", "1.22", "v1.1.0", ""))
publish("example.com/b", "v1.0.0", user("b", "1.22", "v1.3.0", "\nreplace example.com/c => ../c-fork\n"))
// Те же a и b, но с go 1.16 в go.mod — до прореживания графа (Go 1.17).
publish("example.com/olda", "v1.0.0", user("olda", "1.16", "v1.1.0", ""))
publish("example.com/oldb", "v1.0.0", user("oldb", "1.16", "v1.3.0", ""))
publish("example.com/c/v2", "v2.0.0", cFiles("example.com/c/v2", "v2.0.0", "1.22"))
publish("example.com/unused", "v1.0.0", map[string]string{
"go.mod": "module example.com/unused\n\ngo 1.22\n",
"unused.go": "package unused\n\nfunc Hello() string { return \"unused\" }\n",
})
publish("example.com/fresh", "v1.0.0", map[string]string{
"go.mod": "module example.com/fresh\n\ngo 1.25.0\n",
"fresh.go": "package fresh\n\nfunc Hello() string { return \"fresh\" }\n",
})
app := filepath.Join(work, "app")
write(filepath.Join(app, "go.mod"), appMod)
write(filepath.Join(app, "main.go"), appMain)
fmt.Printf("%s %s/%s | модули: go.mod, go.sum, выбор минимальной версии\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
// ── 1 ──────────────────────────────────────────────────────────────
show("1) ВЫБОР МИНИМАЛЬНОЙ ВЕРСИИ")
fmt.Println(" граф: app → a v1.0.0 → c v1.1.0; app → b v1.0.0 → c v1.3.0")
cmd(app, nil, "mod", "tidy")
cmd(app, nil, "list", "-m", "-versions", "example.com/c")
cmd(app, nil, "mod", "graph")
cmd(app, nil, "list", "-m", "all")
cmd(app, nil, "run", ".")
file("go.mod после go mod tidy", filepath.Join(app, "go.mod"))
base := snapshot(app)
// ── 2 ──────────────────────────────────────────────────────────────
show("2) go get: ПОВЫСИТЬ, ПОНИЗИТЬ, -u")
cmd(app, nil, "get", "example.com/c@v1.5.0")
grep(filepath.Join(app, "go.mod"), "example.com/c ", " go.mod: ")
cmd(app, nil, "list", "-m", "example.com/c")
cmd(app, nil, "run", ".")
restore(app, base)
fmt.Println(" понижение; других версий b в прокси нет:")
cmd(app, nil, "get", "example.com/c@v1.1.0")
grep(filepath.Join(app, "go.mod"), "example.com/", " go.mod: ")
cmd(app, nil, "run", ".")
restore(app, base)
cmd(app, nil, "get", "-u", "./...")
grep(filepath.Join(app, "go.mod"), "example.com/c ", " go.mod: ")
restore(app, base)
fmt.Println(" go get без версии и с @latest:")
cmd(app, nil, "get", "example.com/c")
grep(filepath.Join(app, "go.mod"), "example.com/c ", " go.mod: ")
restore(app, base)
cmd(app, nil, "get", "example.com/c@latest")
grep(filepath.Join(app, "go.mod"), "example.com/c ", " go.mod: ")
restore(app, base)
fmt.Println(" понижение; в прокси добавлен b v0.9.0, он требует c v1.1.0:")
publish("example.com/b", "v0.9.0", user("b", "1.22", "v1.1.0", ""))
cmd(app, nil, "get", "example.com/c@v1.1.0")
grep(filepath.Join(app, "go.mod"), "example.com/", " go.mod: ")
cmd(app, nil, "run", ".")
restore(app, base)
// ── 3 ──────────────────────────────────────────────────────────────
show("3) go mod tidy И // indirect")
fmt.Println(" лишнее требование: в go.mod дописан example.com/unused, импорта нет")
cmd(app, nil, "mod", "edit", "-require=example.com/unused@v1.0.0")
cmd(app, nil, "mod", "tidy", "-v")
grep(filepath.Join(app, "go.mod"), "unused", " go.mod: ")
fmt.Println(" импорт без требования: добавлен файл extra.go с import \"example.com/unused\"")
write(filepath.Join(app, "extra.go"), "package main\n\nimport \"example.com/unused\"\n\nvar _ = unused.Hello\n")
cmd(app, nil, "build", "-o", os.DevNull, ".")
cmd(app, nil, "mod", "tidy")
grep(filepath.Join(app, "go.mod"), "unused", " go.mod: ")
os.Remove(filepath.Join(app, "extra.go"))
restore(app, base)
fmt.Println(" прямой импорт c: добавлен файл extra.go с import \"example.com/c\"")
grep(filepath.Join(app, "go.mod"), "example.com/c ", " go.mod до: ")
write(filepath.Join(app, "extra.go"), "package main\n\nimport \"example.com/c\"\n\nvar _ = c.Version\n")
cmd(app, nil, "mod", "tidy")
grep(filepath.Join(app, "go.mod"), "example.com/c ", " go.mod после: ")
os.Remove(filepath.Join(app, "extra.go"))
restore(app, base)
// ── 4 ──────────────────────────────────────────────────────────────
show("4) go.sum: ХЕШИ И ПОДМЕНА")
file("go.sum после go mod tidy", filepath.Join(app, "go.sum"))
n := strings.Count(strings.TrimSpace(base.sum), "\n") + 1
fmt.Printf(" строк в go.sum: %d\n", n)
fmt.Println(" те же требования, но a и b объявляют в go.mod go 1.16 (olda, oldb):")
old := filepath.Join(work, "old")
write(filepath.Join(old, "go.mod"), "module example.com/old\n\ngo 1.24\n\nrequire (\n\texample.com/olda v1.0.0\n\texample.com/oldb v1.0.0\n)\n")
write(filepath.Join(old, "main.go"), "package main\n\nimport (\n\t\"fmt\"\n\n\t\"example.com/olda\"\n\t\"example.com/oldb\"\n)\n\nfunc main() { fmt.Println(olda.CVersion(), oldb.CVersion()) }\n")
cmd(old, nil, "mod", "tidy")
fmt.Println(" строки go.sum про example.com/c:")
grep(filepath.Join(old, "go.sum"), "example.com/c ", " ")
oldSum := strings.TrimSpace(read(filepath.Join(old, "go.sum")))
fmt.Printf(" строк в go.sum: %d\n", strings.Count(oldSum, "\n")+1)
fmt.Println(" без go.sum:")
os.Remove(filepath.Join(app, "go.sum"))
cmd(app, nil, "build", "-o", os.DevNull, ".")
restore(app, base)
fmt.Println(" в go.sum испорчен один символ хеша c v1.3.0 (h1:… модуля, не /go.mod):")
const key = "example.com/c v1.3.0 h1:"
at := strings.Index(base.sum, key) + len(key)
swap := byte('A')
if base.sum[at] == 'A' {
swap = 'B'
}
write(filepath.Join(app, "go.sum"), base.sum[:at]+string(swap)+base.sum[at+1:])
fmt.Println(" кеш модулей тёплый:")
cmd(app, nil, "build", "-o", os.DevNull, ".")
cleanCache()
fmt.Println(" кеш модулей очищен:")
cmd(app, nil, "build", "-o", os.DevNull, ".")
restore(app, base)
fmt.Println(" в прокси подменено содержимое c v1.3.0 (та же версия, другой код), go.sum прежний:")
tampered := cFiles("example.com/c", "v1.3.0", "1.22")
tampered["c.go"] = "package c\n\nfunc Version() string { return \"v1.3.0, но другой код\" }\n"
publish("example.com/c", "v1.3.0", tampered)
cleanCache()
cmd(app, nil, "build", "-o", os.DevNull, ".")
publish("example.com/c", "v1.3.0", cFiles("example.com/c", "v1.3.0", "1.22"))
cleanCache()
if r := run(app, nil, "build", "-o", os.DevNull, "."); r.code != 0 {
panic(r.out)
}
fmt.Println(" исходное содержимое возвращено в прокси: go build снова проходит")
// ── 5 ──────────────────────────────────────────────────────────────
show("5) /v2: ДВЕ МАЖОРНЫЕ ВЕРСИИ В ОДНОЙ СБОРКЕ")
two := filepath.Join(work, "two")
write(filepath.Join(two, "go.mod"), "module example.com/two\n\ngo 1.24\n")
write(filepath.Join(two, "main.go"), `package main
import (
"fmt"
"example.com/c"
cv2 "example.com/c/v2"
)
func main() {
fmt.Println("c.Version() =", c.Version())
fmt.Println("cv2.Version() =", cv2.Version())
}
`)
fmt.Println(" main.go импортирует \"example.com/c\" и cv2 \"example.com/c/v2\"")
cmd(two, nil, "mod", "tidy")
cmd(two, nil, "list", "-m", "all")
cmd(two, nil, "run", ".")
cmd(two, nil, "list", "-m", "-versions", "example.com/c", "example.com/c/v2")
fmt.Println(" v2 под путём без /v2 — требование дописано в go.mod руками:")
cmd(two, nil, "mod", "edit", "-require=example.com/c@v2.0.0")
cmd(two, nil, "build", "-o", os.DevNull, ".")
// ── 6 ──────────────────────────────────────────────────────────────
show("6) replace: ЛОКАЛЬНЫЙ КАТАЛОГ")
write(filepath.Join(work, "c-local", "go.mod"), "module example.com/c\n\ngo 1.22\n")
write(filepath.Join(work, "c-local", "c.go"), "package c\n\nfunc Version() string { return \"local copy\" }\n")
cmd(app, nil, "mod", "edit", "-replace=example.com/c=../c-local")
grep(filepath.Join(app, "go.mod"), "replace", " go.mod: ")
cmd(app, nil, "list", "-m", "example.com/c")
cmd(app, nil, "run", ".")
cmd(app, nil, "mod", "tidy")
fmt.Println(" строки go.sum про example.com/c после tidy:")
grep(filepath.Join(app, "go.sum"), "example.com/c ", " ")
restore(app, base)
fmt.Println(" replace в главном модуле на каталог, которого нет:")
cmd(app, nil, "mod", "edit", "-replace=example.com/c=../c-missing")
cmd(app, nil, "build", "-o", os.DevNull, ".")
restore(app, base)
fmt.Println(" replace в go.mod зависимости: go.mod модуля b из прокси —")
for _, l := range strings.Split(strings.TrimRight(read(filepath.Join(proxy, "example.com/b/@v/v1.0.0.mod")), "\n"), "\n") {
if l != "" {
fmt.Println(" " + l)
}
}
fmt.Println(" каталога ../c-fork нет нигде, и это не ошибка:")
cmd(app, nil, "run", ".")
// ── 7 ──────────────────────────────────────────────────────────────
show("7) СТРОКИ go И toolchain")
lang := filepath.Join(work, "lang")
write(filepath.Join(lang, "main.go"), "package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfor i := range 3 {\n\t\tfmt.Print(i)\n\t}\n\tfmt.Println()\n}\n")
for _, g := range []string{"1.21", "1.22"} {
write(filepath.Join(lang, "go.mod"), "module example.com/lang\n\ngo "+g+"\n")
fmt.Printf(" for i := range 3, в go.mod go %s:\n", g)
cmd(lang, nil, "run", ".")
}
newer := filepath.Join(work, "newer")
write(filepath.Join(newer, "main.go"), "package main\n\nfunc main() {}\n")
write(filepath.Join(newer, "go.mod"), "module example.com/newer\n\ngo 1.25.0\n")
fmt.Println(" в go.mod go 1.25.0:")
cmd(newer, []string{"GOTOOLCHAIN=local"}, "build", "-o", os.DevNull, ".")
cmd(newer, []string{"GOTOOLCHAIN=auto"}, "build", "-o", os.DevNull, ".")
write(filepath.Join(newer, "go.mod"), "module example.com/newer\n\ngo 1.24\n\ntoolchain go1.25.1\n")
fmt.Println(" в go.mod go 1.24 и toolchain go1.25.1:")
cmd(newer, []string{"GOTOOLCHAIN=local"}, "env", "GOVERSION")
cmd(newer, []string{"GOTOOLCHAIN=auto"}, "env", "GOVERSION")
fmt.Println(" зависимость, чей go.mod требует go 1.25.0:")
cmd(app, []string{"GOTOOLCHAIN=local"}, "get", "example.com/fresh@v1.0.0")
grep(filepath.Join(app, "go.mod"), "go 1.", " go.mod: ")
grep(filepath.Join(app, "go.mod"), "fresh", " go.mod: ")
restore(app, base)
// ── 8 ──────────────────────────────────────────────────────────────
// Разделы 8–10 добавлены после внешнего аудита 14.10.2026 — в конец,
// чтобы не сдвигать вывод, который урок уже цитирует.
show("8) МИНИМАЛЬНЫЙ МОДУЛЬ: go mod init И go run")
hello := filepath.Join(work, "hello")
write(filepath.Join(hello, "main.go"), helloMain)
fmt.Println(" в пустом каталоге — один файл main.go:")
for _, l := range strings.Split(strings.TrimRight(helloMain, "\n"), "\n") {
if l == "" {
fmt.Println()
continue
}
fmt.Println(" " + strings.ReplaceAll(l, "\t", " "))
}
cmd(hello, nil, "mod", "init", "example.com/hello")
cmd(hello, nil, "run", ".")
file("go.mod после go mod init", filepath.Join(hello, "go.mod"))
// ── 9 ──────────────────────────────────────────────────────────────
show("9) go build -mod=mod: ПРАВИТЬ go.mod РАЗРЕШЕНО ЯВНО")
fmt.Println(" импорт без требования: добавлен файл extra.go с import \"example.com/unused\"")
write(filepath.Join(app, "extra.go"), "package main\n\nimport \"example.com/unused\"\n\nvar _ = unused.Hello\n")
cmd(app, nil, "build", "-o", os.DevNull, ".")
cmd(app, nil, "build", "-mod=mod", "-o", os.DevNull, ".")
grep(filepath.Join(app, "go.mod"), "unused", " go.mod: ")
os.Remove(filepath.Join(app, "extra.go"))
restore(app, base)
// ── 10 ─────────────────────────────────────────────────────────────
show("10) replace: РАБОЧЕЕ ПРОСТРАНСТВО И ТРЕБОВАНИЯ ЗАМЕНЫ")
fmt.Println(" go.work: use ./app и ./b-dev; b-dev — копия b с replace example.com/c => ../c-local")
bdev := filepath.Join(work, "b-dev")
for name, body := range user("b", "1.22", "v1.3.0", "\nreplace example.com/c => ../c-local\n") {
write(filepath.Join(bdev, name), body)
}
write(filepath.Join(work, "go.work"), "go 1.24\n\nuse (\n\t./app\n\t./b-dev\n)\n")
ws := []string{"GOWORK=" + filepath.Join(work, "go.work")}
cmd(app, ws, "list", "-m", "example.com/b", "example.com/c")
cmd(app, ws, "run", ".")
fmt.Println(" без go.work (GOWORK=off) — тот же app:")
cmd(app, nil, "run", ".")
fmt.Println(" замена со своими требованиями: go.mod каталога c-req требует example.com/unused")
write(filepath.Join(work, "c-req", "go.mod"), "module example.com/c\n\ngo 1.22\n\nrequire example.com/unused v1.0.0\n")
write(filepath.Join(work, "c-req", "c.go"), "package c\n\nimport \"example.com/unused\"\n\nfunc Version() string { return \"c-req + \" + unused.Hello() }\n")
cmd(app, nil, "list", "-m", "all")
cmd(app, nil, "mod", "edit", "-replace=example.com/c=../c-req")
cmd(app, nil, "mod", "tidy")
cmd(app, nil, "list", "-m", "all")
cmd(app, nil, "run", ".")
restore(app, base)
}
// helloMain — полный файл минимального модуля из раздела 8: только
// стандартная библиотека, поэтому его можно повторить в любом пустом каталоге.
const helloMain = `package main
import (
"fmt"
"runtime/debug"
)
func main() {
info, _ := debug.ReadBuildInfo()
fmt.Println("module:", info.Main.Path)
}
`