ЗАМЕР
bench/gomod/practice.go
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/golang/modules
- Как запустить
go run bench/gomod/internals.go /usr/local/go1.25.1/bin/go run bench/gomod/internals.go go run bench/gomod/practice.go
Запись прогона
Замеры для урока «Модули и зависимости»
| Файл | Что делает |
|---|---|
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.
Скрипт
204 строк//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)
}