Deep Engineering

MEASUREMENT

bench/gomod/practice.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

204 lines
//go:build ignore

// Источник ответов двух практических задач урока «Модули и зависимости».
//
// ЗАДАЧА 1 — предсказать, какую версию z увидят модули x и y, если x
// требует z v1.2.0, y — z v1.4.0, а в прокси есть и v1.7.0.
//
// ЗАДАЧА 2 — сколько строк окажется в go.sum после go mod tidy.
//
// Граф собирается в файловом прокси во временном каталоге, как в
// internals.go; команды — настоящий go той же версии, что запустила этот
// файл, со своими GOMODCACHE и GOCACHE, GOSUMDB=off и GOTOOLCHAIN=local.
// Показанный в уроке код — дословно строки ниже.
//
// ЗАПУСК:
//
//	go run bench/gomod/practice.go
package main

import (
	"archive/zip"
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
)

// go.mod модуля x
// go.mod of module x
var xMod = strings.TrimPrefix(`
module example.com/x

go 1.22

require (
	example.com/z v1.2.0
	example.com/w v1.0.0
)
`, "\n")

// go.mod модуля y
// go.mod of module y
var yMod = strings.TrimPrefix(`
module example.com/y

go 1.22

require example.com/z v1.4.0
`, "\n")

const appMod = `module example.com/app

go 1.24

require (
	example.com/x v1.0.0
	example.com/y v1.0.0
)
`

// main.go приложения
// main.go of the app
const appMain = `package main

import (
	"fmt"

	"example.com/x"
	"example.com/y"
)

func main() {
	fmt.Println("x:", x.ZVersion())
	fmt.Println("y:", y.ZVersion())
}
`

// в прокси у z есть v1.2.0, v1.4.0 и v1.7.0
// the proxy has z v1.2.0, v1.4.0 and v1.7.0
var zVersions = []string{"v1.2.0", "v1.4.0", "v1.7.0"}

var (
	goBin = filepath.Join(runtime.GOROOT(), "bin", "go")
	work  string
	env   []string
)

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)
	}
}

// publish — то же, что в internals.go: .mod, .info, .zip и list.
func publish(mod, ver string, files map[string]string) {
	dir := filepath.Join(work, "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())
	old, _ := os.ReadFile(filepath.Join(dir, "list"))
	write(filepath.Join(dir, "list"), string(old)+ver+"\n")
}

func goCmd(dir string, args ...string) string {
	cmd := exec.Command(goBin, args...)
	cmd.Dir = dir
	cmd.Env = env
	out, err := cmd.CombinedOutput()
	if err != nil {
		panic(fmt.Sprintf("go %s: %v\n%s", strings.Join(args, " "), err, out))
	}
	return string(out)
}

func main() {
	var err error
	work, err = os.MkdirTemp("", "gomod-practice")
	if err != nil {
		panic(err)
	}
	for _, e := range os.Environ() {
		if !strings.HasPrefix(e, "GO") {
			env = append(env, e)
		}
	}
	env = append(env,
		"GOPROXY=file://"+filepath.Join(work, "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"),
	)
	defer func() {
		goCmd(work, "clean", "-modcache")
		os.RemoveAll(work)
	}()

	for _, v := range zVersions {
		publish("example.com/z", v, map[string]string{
			"go.mod": "module example.com/z\n\ngo 1.22\n",
			"z.go":   "package z\n\nfunc Version() string { return \"" + v + "\" }\n",
		})
	}
	publish("example.com/w", "v1.0.0", map[string]string{
		"go.mod": "module example.com/w\n\ngo 1.22\n",
		"w.go":   "package w\n\nfunc Name() string { return \"w\" }\n",
	})
	publish("example.com/x", "v1.0.0", map[string]string{
		"go.mod": xMod,
		"x.go": "package x\n\nimport (\n\t\"example.com/w\"\n\t\"example.com/z\"\n)\n\n" +
			"func ZVersion() string { _ = w.Name(); return z.Version() }\n",
	})
	publish("example.com/y", "v1.0.0", map[string]string{
		"go.mod": yMod,
		"y.go":   "package y\n\nimport \"example.com/z\"\n\nfunc ZVersion() string { return z.Version() }\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.sum\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
	goCmd(app, "mod", "tidy")

	fmt.Println()
	fmt.Println("ЗАДАЧА 1: go run . и go list -m example.com/z")
	fmt.Print(goCmd(app, "run", "."))
	fmt.Print(goCmd(app, "list", "-m", "example.com/z"))

	fmt.Println()
	fmt.Println("ЗАДАЧА 2: go.sum после go mod tidy")
	sum, err := os.ReadFile(filepath.Join(app, "go.sum"))
	if err != nil {
		panic(err)
	}
	fmt.Print(string(sum))
	n := strings.Count(strings.TrimSpace(string(sum)), "\n") + 1
	fmt.Printf("строк в go.sum: %d\n", n)
}