MEASUREMENT
bench/gotool/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/tooling
- How to run it
go run bench/gotool/internals.go /usr/local/go1.25.1/bin/go run bench/gotool/internals.go go run bench/gotool/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
Замеры для урока «Инструменты go»
| Файл | Что делает |
|---|---|
internals.go |
одиннадцать наблюдений, каждое — запуск команды go или gofmt на программах из cases/: кеш сборки, где лежит бинарник go run и какой у него код выхода, gofmt -l и -d, компилятор против go vet, подмножество vet внутри go test, golangci-lint сверх vet, ограничения сборки, кросс-компиляция, -ldflags "-X …" и -s -w, go generate, ключ кеша |
practice.go |
источник ответов двух практических задач урока: вывод программы, собранной с -X (константное выражение и ссылка на переменную в инициализаторе), и во сколько раз бинарник HTTP-сервера на net/http больше hello на fmt |
cases/ |
отдельный модуль с подопытными программами; часть намеренно не собирается или не проходит vet |
cases/hello |
fmt и os: кеш, go run, код выхода, кросс-компиляция, размер, паника в бинарнике без символов |
cases/messy, cases/brace |
неотформатированный файл для gofmt -d и открывающая скобка на своей строке |
cases/unusedvar, printf, copylock, unreachable, unusedresult, errcheck, ineffassign |
по одной ошибке, на которой сравниваются компилятор, go vet и golangci-lint |
cases/testvet/printf, cases/testvet/copylock |
пакеты с тестами: какую ошибку vet ловит go test, а какую нет |
cases/tags |
файлы с суффиксами _linux, _windows и строками //go:build |
cases/stamp |
переменные для -ldflags "-X …": неинициализированная, со строкой, с вызовом функции |
cases/gen |
//go:generate go run gen.go; генератор пишет codes_gen.go (в копии во временном каталоге) |
go run bench/gotool/internals.go
/usr/local/go1.25.1/bin/go run bench/gotool/internals.go
go run bench/gotool/practice.go
internals.go работает со своим кешем сборки во временном каталоге
(GOCACHE), поэтому холодная сборка в разделе 1 действительно холодная, а
кеш пользователя не трогается. Раздел 6 нужен golangci-lint в
/usr/local/bin; без него раздел пропускается. Прогон на 2 ядрах занял
около двух минут: раздел 1 трижды собирает стандартную библиотеку с нуля, а
раздел 8 — ещё для трёх новых пар GOOS/GOARCH.
Записи прогонов — в runs/: internals.txt и practice.txt — go1.24.7
linux/amd64, internals-go1.25.txt — go1.25.1. Машина: 2 ядра. Временные
пути заменены плейсхолдерами <tmp>, <GOCACHE>, <home>, <cases>.
Что получилось
Повторная сборка не компилирует ничего. Холодная сборка cases/hello
вызвала компилятор 55 раз (54 пакета стандартной библиотеки и main; на
go1.25.1 — 60) и заняла 6,6 с; с тёплым кешем — ни одного вызова
компилятора, 0,16 с. Готовых скомпилированных пакетов в $GOROOT/pkg нет. Если бинарник уже собран из тех же
входов, go build не вызывает и компоновщик. touch файла кеш не
сбрасывает, правка комментария — перекомпилирует один пакет.
go run — это сборка во временный каталог без отладочной информации.
Первый go run положил бинарник в <tmp>/go-build<N>/b001/exe/hello, и после
выхода файла нет. Второй запустил бинарник прямо из кеша сборки. Размер
бинарника go run совпал с go build -ldflags "-s -w" до байта (сравнивался
только размер). Код
выхода программы 3 превратился в код выхода go run 1.
Компилятор пропускает то, что ловит vet. Printf("%d", string), копия
sync.Mutex в получателе-значении, недостижимый код, выброшенный результат
fmt.Sprintf — всё собралось. go vet поймал все четыре. Неиспользуемую
переменную ловит сам компилятор. Проигнорированную ошибку os.Remove и
лишнее присваивание vet не видит — их нашёл golangci-lint 2.5.0
(errcheck, ineffassign).
go test запускает vet, но не весь. Тест пакета с неверным глаголом
Printf не запустился: [build failed]. Тест пакета с копией мьютекса прошёл
и напечатал after Inc: 0: copylocks не входит в подмножество, которое
go test запускает по умолчанию. С -vet=all — [build failed].
Кросс-компиляция — две переменные окружения. GOOS=windows дал файл PE,
GOOS=darwin GOARCH=arm64 — Mach-O; стандартная библиотека для новой пары
собрана из исходников (54–57 пакетов), CGO_ENABLED при этом 0.
-X молчит, когда не срабатывает. Переменная с вызовом функции в
инициализаторе осталась unknown; опечатка в имени переменной — код выхода 0
и никакого сообщения. -s -w уменьшил бинарник в 1,54 раза (-s один дал
тот же размер: по документации он включает -w), а паника в нём
по-прежнему печатает main.main() и строку исходника.
gofmt — не про вкус. Открывающая скобка на своей строке — синтаксическая
ошибка, а не стиль: unexpected semicolon or newline before {.
Script
533 lines//go:build ignore
// Инструменты go: что на самом деле делают go run, go build, gofmt, go vet
// и флаги сборки.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Вопросы об инструментах на собеседовании звучат как
// бытовые — «чем go run отличается от go build», «зачем go vet, если есть
// компилятор». Ответы, которые отличают знающего, — о поведении: где лежит
// бинарник go run, что именно кешируется, какие ошибки компилятор пропускает,
// какие проверки vet запускает go test, что делает -ldflags. Здесь каждое
// такое поведение получено запуском команды go, а не пересказано.
//
// ОДИННАДЦАТЬ СЮЖЕТОВ:
//
// 1. Кеш сборки: холодная сборка компилирует десятки пакетов, повторная — ни
// одного.
// 2. go run: бинарник собирается во временный каталог, без отладочной
// информации; код выхода программы go run не передаёт.
// 3. gofmt -l и -d: что именно он меняет; скобка на новой строке — не стиль,
// а синтаксическая ошибка.
// 4. Компилятор против go vet на семи подопытных программах.
// 5. go vet внутри go test: только подмножество проверок.
// 6. golangci-lint: что он ловит сверх vet.
// 7. Ограничения сборки: суффикс имени файла и строка //go:build.
// 8. Кросс-компиляция: GOOS и GOARCH, без внешних инструментов.
// 9. -ldflags: подстановка версии через -X и размер бинарника с -s -w.
// 10. go generate: команды из комментариев запускаются только по явному
// вызову.
// 11. Ключ кеша — содержимое файла, а не время его изменения.
//
// Подопытные программы лежат в cases/ — отдельный модуль со своим go.mod.
// Часть из них намеренно не собирается или не проходит vet.
//
// Программа работает со СВОИМ кешем сборки во временном каталоге (GOCACHE),
// чтобы холодная сборка была действительно холодной. Кеш пользователя она
// не трогает. Временные пути в выводе заменены плейсхолдерами.
//
// ЗАПУСК:
//
// go run bench/gotool/internals.go
// /usr/local/go1.25.1/bin/go run bench/gotool/internals.go
package main
import (
"debug/elf"
"debug/macho"
"debug/pe"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"time"
)
var (
here string // bench/gotool
cases string // bench/gotool/cases
work string // временный каталог программы
gocache string // свой кеш сборки
goBin string // go той же версии, что запустила программу
baseEnv []string
)
type result struct {
out string
code int
dur time.Duration
}
// run запускает команду в каталоге dir с добавочными переменными окружения.
func run(dir string, env []string, name string, args ...string) result {
cmd := exec.Command(name, args...)
cmd.Dir = dir
cmd.Env = append(append([]string{}, baseEnv...), env...)
start := time.Now()
out, err := cmd.CombinedOutput()
d := time.Since(start)
code := 0
if ee, ok := err.(*exec.ExitError); ok {
code = ee.ExitCode()
} else if err != nil {
panic(err)
}
return result{string(out), code, d}
}
func goRun(dir string, env []string, args ...string) result { return run(dir, env, goBin, args...) }
var (
reGoBuild = regexp.MustCompile(`/tmp/go-build\d+`)
reHash = regexp.MustCompile(`/[0-9a-f]{2}/[0-9a-f]{64}-d/`)
reTiming = regexp.MustCompile(`\t\d+\.\d+s$|\s*\(\d+\.\d+s\)`)
reOffset = regexp.MustCompile(`\+0x[0-9a-f]+`)
)
// clean заменяет временные пути плейсхолдерами: вывод должен быть одинаковым
// от запуска к запуску.
func clean(s string) string {
s = strings.ReplaceAll(s, gocache, "<GOCACHE>")
s = strings.ReplaceAll(s, cases, "<cases>")
s = reOffset.ReplaceAllString(s, "+0x<N>")
s = strings.ReplaceAll(s, work, "<tmp>")
s = reGoBuild.ReplaceAllString(s, "<tmp>/go-build<N>")
s = reHash.ReplaceAllString(s, "/<xx>/<hash>-d/")
if home, err := os.UserHomeDir(); err == nil && home != "/" {
s = strings.ReplaceAll(s, home, "<home>")
}
return s
}
// lines печатает строки вывода с отступом, пропуская строки «# пакет».
func lines(out string, indent string) {
for _, l := range strings.Split(strings.TrimRight(out, "\n"), "\n") {
if strings.HasPrefix(l, "#") || strings.TrimSpace(l) == "" {
continue
}
fmt.Println(indent + strings.TrimRight(reTiming.ReplaceAllString(clean(l), ""), " \t"))
}
}
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
// count считает вызовы компилятора и компоновщика в выводе go build -x.
var reTool = regexp.MustCompile(`(?:^|\s)\S*/pkg/tool/[^/\s]+/(compile|link)\s`)
func count(xout string) (compile, link int) {
for _, l := range strings.Split(xout, "\n") {
if m := reTool.FindStringSubmatch(l); m != nil {
if m[1] == "compile" {
compile++
} else {
link++
}
}
}
return
}
// format определяет формат исполняемого файла по его заголовку.
func format(path string) string {
if f, err := elf.Open(path); err == nil {
defer f.Close()
return "ELF, " + f.Machine.String()
}
if f, err := pe.Open(path); err == nil {
defer f.Close()
m := map[uint16]string{pe.IMAGE_FILE_MACHINE_AMD64: "IMAGE_FILE_MACHINE_AMD64", pe.IMAGE_FILE_MACHINE_ARM64: "IMAGE_FILE_MACHINE_ARM64"}
return "PE, " + m[f.Machine]
}
if f, err := macho.Open(path); err == nil {
defer f.Close()
return "Mach-O, " + f.Cpu.String()
}
return "неизвестный формат"
}
// debugInfo: есть ли в ELF таблица символов и отладочная информация DWARF.
func debugInfo(path string) string {
f, err := elf.Open(path)
if err != nil {
return "?"
}
defer f.Close()
yes := func(name string) string {
if f.Section(name) != nil {
return "есть"
}
return "нет"
}
return fmt.Sprintf(".symtab %s, .debug_info %s", yes(".symtab"), yes(".debug_info"))
}
func size(path string) int64 {
st, err := os.Stat(path)
if err != nil {
panic(err)
}
return st.Size()
}
// spaced печатает число с пробелами между разрядами: 2 297 638.
func spaced(n int64) string {
s := fmt.Sprint(n)
var b strings.Builder
for i, c := range s {
if i > 0 && (len(s)-i)%3 == 0 {
b.WriteByte(' ')
}
b.WriteRune(c)
}
return b.String()
}
func best(n int, f func() time.Duration) time.Duration {
lo := time.Duration(1 << 62)
for i := 0; i < n; i++ {
if d := f(); d < lo {
lo = d
}
}
return lo
}
// copyDir копирует плоский каталог с исходниками.
func copyDir(src, dst string) {
if err := os.MkdirAll(dst, 0o755); err != nil {
panic(err)
}
entries, err := os.ReadDir(src)
if err != nil {
panic(err)
}
for _, e := range entries {
if e.IsDir() {
continue
}
b, err := os.ReadFile(filepath.Join(src, e.Name()))
if err != nil {
panic(err)
}
if err := os.WriteFile(filepath.Join(dst, e.Name()), b, 0o644); err != nil {
panic(err)
}
}
}
func main() {
_, file, _, _ := runtime.Caller(0)
here = filepath.Dir(file)
cases = filepath.Join(here, "cases")
goBin = filepath.Join(runtime.GOROOT(), "bin", "go")
if _, err := os.Stat(goBin); err != nil {
goBin = "go"
}
var err error
work, err = os.MkdirTemp("", "gotool")
if err != nil {
panic(err)
}
defer os.RemoveAll(work)
gocache = filepath.Join(work, "cache")
// Тот же go первым в PATH: на него опирается golangci-lint и go generate.
baseEnv = append(os.Environ(),
"GOCACHE="+gocache,
"GOTOOLCHAIN=local",
"GOFLAGS=",
"PATH="+filepath.Dir(goBin)+string(os.PathListSeparator)+os.Getenv("PATH"),
"GOLANGCI_LINT_CACHE="+filepath.Join(work, "lintcache"),
)
fmt.Printf("%s %s/%s | инструменты go: run, build, fmt, vet\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
// ── 1 ──────────────────────────────────────────────────────────────
show("1) КЕШ СБОРКИ: ХОЛОДНАЯ И ПОВТОРНАЯ СБОРКА")
def := run(cases, []string{"GOCACHE="}, goBin, "env", "GOCACHE")
fmt.Println(" go env GOCACHE (по умолчанию) -> " + clean(strings.TrimSpace(def.out)))
fmt.Println(" программа: cases/hello — fmt и os, ничего больше")
// Есть ли в поставке готовые скомпилированные пакеты стандартной
// библиотеки: что лежит в $GOROOT/pkg.
pkgDir, _ := os.ReadDir(filepath.Join(runtime.GOROOT(), "pkg"))
var pkgNames []string
for _, e := range pkgDir {
pkgNames = append(pkgNames, e.Name())
}
fmt.Printf(" каталог $GOROOT/pkg поставки: %s\n", strings.Join(pkgNames, " "))
out := filepath.Join(work, "hello")
var coldC, coldL int
cold := best(3, func() time.Duration {
os.RemoveAll(gocache)
os.Remove(out)
r := goRun(cases, nil, "build", "-x", "-o", out, "./hello")
coldC, coldL = count(r.out)
return r.dur
})
var warmC, warmL int
warm := best(5, func() time.Duration {
os.Remove(out)
r := goRun(cases, nil, "build", "-x", "-o", out, "./hello")
warmC, warmL = count(r.out)
return r.dur
})
var sameC, sameL int
same := best(5, func() time.Duration {
r := goRun(cases, nil, "build", "-x", "-o", out, "./hello")
sameC, sameL = count(r.out)
return r.dur
})
fmt.Println(" go build -o hello ./hello:")
fmt.Printf(" холодный кеш %5.1f с compile: %2d, link: %d\n", cold.Seconds(), coldC, coldL)
fmt.Printf(" тёплый кеш, файла hello ещё нет %5.2f с compile: %2d, link: %d\n", warm.Seconds(), warmC, warmL)
fmt.Printf(" тёплый кеш, hello уже собран %5.2f с compile: %2d, link: %d\n", same.Seconds(), sameC, sameL)
fmt.Printf(" холодная / тёплая: %4.0f\n", cold.Seconds()/warm.Seconds())
fmt.Println(" Лучший из 3 (холодный кеш очищается перед каждым запуском)")
fmt.Println(" и из 5 (тёплый). Счёт вызовов — по строкам go build -x.")
fmt.Println(" Пакеты берутся из кеша; бинарник, которого нет, компонуется")
fmt.Println(" заново; бинарник, который уже собран из тех же входов, не")
fmt.Println(" трогается вовсе.")
// ── 2 ──────────────────────────────────────────────────────────────
show("2) go run: ВРЕМЕННЫЙ КАТАЛОГ И КОД ВЫХОДА")
for _, label := range []string{"первый go run", "второй go run"} {
r := goRun(cases, nil, "run", "-x", "./hello")
c, l := count(r.out)
exe := ""
for _, ln := range strings.Split(r.out, "\n") {
if strings.HasPrefix(ln, "exe: ") {
exe = strings.TrimPrefix(ln, "exe: ")
}
}
_, statErr := os.Stat(exe)
fmt.Printf(" %s compile: %d, link: %d\n", label, c, l)
fmt.Printf(" бинарник: %s\n", clean(exe))
fmt.Printf(" после выхода файл на месте: %v\n", statErr == nil)
}
fail := goRun(cases, nil, "run", "./hello", "fail")
fmt.Println(" go run ./hello fail:")
lines(fail.out, " ")
fmt.Printf(" код выхода go run: %d\n", fail.code)
direct := run(cases, nil, filepath.Join(work, "hello"), "fail")
fmt.Printf(" код выхода самой программы: %d\n", direct.code)
// ── 3 ──────────────────────────────────────────────────────────────
show("3) gofmt -l И gofmt -d")
gofmt := filepath.Join(filepath.Dir(goBin), "gofmt")
fmt.Println(" gofmt -l messy/ tags/")
lines(run(cases, nil, gofmt, "-l", "messy", "tags").out, " ")
fmt.Println(" go build ./messy/")
if r := goRun(cases, nil, "build", "-o", os.DevNull, "./messy"); r.code == 0 {
fmt.Println(" собрано, ошибок нет")
}
fmt.Println(" gofmt -d messy/main.go")
lines(run(cases, nil, gofmt, "-d", "messy/main.go").out, " ")
fmt.Println(" gofmt -l brace/ (открывающая скобка на своей строке)")
lines(run(cases, nil, gofmt, "-l", "brace").out, " ")
fmt.Println(" go build ./brace/")
lines(goRun(cases, nil, "build", "-o", os.DevNull, "./brace").out, " ")
// ── 4 ──────────────────────────────────────────────────────────────
show("4) КОМПИЛЯТОР ПРОТИВ go vet")
vetCases := []string{"unusedvar", "printf", "copylock", "unreachable", "unusedresult", "errcheck", "ineffassign"}
for _, c := range vetCases {
fmt.Println(" " + c + ":")
b := goRun(filepath.Join(cases, c), nil, "build", "-o", os.DevNull, ".")
if b.code == 0 {
fmt.Println(" go build: собрано, ошибок нет")
} else {
lines(b.out, " go build: ")
}
v := goRun(filepath.Join(cases, c), nil, "vet", ".")
if v.code == 0 && strings.TrimSpace(v.out) == "" {
fmt.Println(" go vet: замечаний нет")
} else {
lines(v.out, " go vet: ")
}
}
cl := goRun(cases, nil, "run", "./copylock")
fmt.Printf(" copylock, запуск: c.Inc(), затем c.n -> %s\n", strings.TrimSpace(cl.out))
// ── 5 ──────────────────────────────────────────────────────────────
show("5) go vet ВНУТРИ go test")
fmt.Println(" go test ./testvet/printf/")
lines(goRun(cases, nil, "test", "-count=1", "./testvet/printf/").out, " ")
fmt.Println(" go test -v ./testvet/copylock/")
lines(goRun(cases, nil, "test", "-count=1", "-v", "./testvet/copylock/").out, " ")
fmt.Println(" go test -vet=all ./testvet/copylock/")
lines(goRun(cases, nil, "test", "-count=1", "-vet=all", "./testvet/copylock/").out, " ")
// ── 6 ──────────────────────────────────────────────────────────────
show("6) golangci-lint СВЕРХ go vet")
lint := "/usr/local/bin/golangci-lint"
if _, err := os.Stat(lint); err != nil {
fmt.Println(" golangci-lint не найден — раздел пропущен")
} else {
fmt.Println(" " + strings.TrimSpace(run(cases, nil, lint, "--version").out))
fmt.Println(" golangci-lint run --no-config (линтеры по умолчанию):")
issue := regexp.MustCompile(`^\S+\.go:\d+(:\d+)?: .*\(\w+\)$`)
for _, c := range vetCases {
r := run(filepath.Join(cases, c), nil, lint, "run", "--no-config", "./...")
fmt.Println(" " + c + ":")
found := false
for _, l := range strings.Split(r.out, "\n") {
if issue.MatchString(l) {
fmt.Println(" " + clean(l))
found = true
}
}
if !found {
fmt.Println(" замечаний нет")
}
}
}
// ── 7 ──────────────────────────────────────────────────────────────
show("7) ОГРАНИЧЕНИЯ СБОРКИ: ИМЯ ФАЙЛА И //go:build")
all, _ := filepath.Glob(filepath.Join(cases, "tags", "*.go"))
for i := range all {
all[i] = filepath.Base(all[i])
}
sort.Strings(all)
fmt.Printf(" файлы в tags/: %s\n", strings.Join(all, " "))
for _, v := range []struct {
label string
env []string
args []string
}{
{"linux (по умолчанию)", nil, nil},
{"GOOS=windows", []string{"GOOS=windows"}, nil},
{"GOOS=darwin", []string{"GOOS=darwin"}, nil},
{"-tags debug", nil, []string{"-tags", "debug"}},
} {
args := append([]string{"list"}, v.args...)
args = append(args, "-f", "{{.GoFiles}}", "./tags")
r := goRun(cases, v.env, args...)
fmt.Printf(" %-22s %s\n", v.label, strings.TrimSpace(r.out))
}
fmt.Println(" go run ./tags")
lines(goRun(cases, nil, "run", "./tags").out, " ")
fmt.Println(" go run -tags debug ./tags")
lines(goRun(cases, nil, "run", "-tags", "debug", "./tags").out, " ")
// ── 8 ──────────────────────────────────────────────────────────────
show("8) КРОСС-КОМПИЛЯЦИЯ: GOOS И GOARCH")
for _, t := range []struct{ goos, goarch, out string }{
{"linux", "amd64", "hello-linux-amd64"},
{"linux", "arm64", "hello-linux-arm64"},
{"windows", "amd64", "hello.exe"},
{"darwin", "arm64", "hello-darwin-arm64"},
} {
env := []string{"GOOS=" + t.goos, "GOARCH=" + t.goarch}
cgo := strings.TrimSpace(goRun(cases, env, "env", "CGO_ENABLED").out)
out := filepath.Join(work, t.out)
r := goRun(cases, env, "build", "-x", "-o", out, "./hello")
if r.code != 0 {
fmt.Println(" ошибка сборки:", clean(r.out))
continue
}
c, _ := count(r.out)
fmt.Printf(" GOOS=%-7s GOARCH=%-5s CGO_ENABLED=%s compile: %2d %-30s %s байт\n",
t.goos, t.goarch, cgo, c, format(out), spaced(size(out)))
}
fmt.Println(" compile — сколько пакетов скомпилировано: для новой пары")
fmt.Println(" GOOS/GOARCH стандартная библиотека собирается из исходников")
fmt.Println(" тем же компилятором; linux/amd64 уже лежал в кеше. Внешних")
fmt.Println(" инструментов не нужно: cgo при кросс-компиляции выключен.")
// ── 9 ──────────────────────────────────────────────────────────────
show("9) -ldflags: ВЕРСИЯ ЧЕРЕЗ -X И РАЗМЕР С -s -w")
fmt.Println(" go run ./stamp")
lines(goRun(cases, nil, "run", "./stamp").out, " ")
fmt.Println(` go run -ldflags "-X main.version=1.4.2 -X main.commit=9f3c2ab -X main.built=2026-10-14" ./stamp`)
lines(goRun(cases, nil, "run", "-ldflags", "-X main.version=1.4.2 -X main.commit=9f3c2ab -X main.built=2026-10-14", "./stamp").out, " ")
fmt.Println(` go run -ldflags "-X main.verison=1.4.2" ./stamp (опечатка в имени)`)
r := goRun(cases, nil, "run", "-ldflags", "-X main.verison=1.4.2", "./stamp")
lines(r.out, " ")
fmt.Printf(" код выхода: %d\n", r.code)
plain := filepath.Join(work, "hello-plain")
stripped := filepath.Join(work, "hello-sw")
onlyS := filepath.Join(work, "hello-s")
goRun(cases, nil, "build", "-o", plain, "./hello")
goRun(cases, nil, "build", "-ldflags", "-s -w", "-o", stripped, "./hello")
goRun(cases, nil, "build", "-ldflags", "-s", "-o", onlyS, "./hello")
// Бинарник второго go run лежит в кеше: его путь печатает сама программа.
var cached string
for _, ln := range strings.Split(goRun(cases, nil, "run", "./hello").out, "\n") {
if strings.HasPrefix(ln, "exe: ") {
cached = strings.TrimPrefix(ln, "exe: ")
}
}
fmt.Println(" размер cases/hello:")
fmt.Printf(" go build %s байт %s\n", spaced(size(plain)), debugInfo(plain))
fmt.Printf(" go build -ldflags \"-s -w\" %s байт %s\n", spaced(size(stripped)), debugInfo(stripped))
fmt.Printf(" go build -ldflags \"-s\" %s байт %s\n", spaced(size(onlyS)), debugInfo(onlyS))
if cached != "" {
fmt.Printf(" бинарник go run из кеша %s байт %s\n", spaced(size(cached)), debugInfo(cached))
}
fmt.Printf(" -s -w меньше в %.2f раза\n", float64(size(plain))/float64(size(stripped)))
fmt.Println(" паника в бинарнике, собранном с -s -w:")
p := run(cases, nil, stripped, "panic")
lines(p.out, " ")
fmt.Printf(" код выхода: %d\n", p.code)
// ── 10 ─────────────────────────────────────────────────────────────
show("10) go generate: ТОЛЬКО ПО ЯВНОМУ ВЫЗОВУ")
gen := filepath.Join(work, "gen")
copyDir(filepath.Join(cases, "gen"), gen)
os.WriteFile(filepath.Join(gen, "go.mod"), []byte("module gen\n\ngo 1.24\n"), 0o644)
fmt.Println(" go build .")
lines(goRun(gen, nil, "build", "-o", os.DevNull, ".").out, " ")
fmt.Println(" go generate -x .")
lines(goRun(gen, nil, "generate", "-x", ".").out, " ")
files, _ := filepath.Glob(filepath.Join(gen, "*.go"))
for i := range files {
files[i] = filepath.Base(files[i])
}
sort.Strings(files)
fmt.Println(" файлы после: " + strings.Join(files, " "))
fmt.Println(" go run .")
lines(goRun(gen, nil, "run", ".").out, " ")
// ── 11 ─────────────────────────────────────────────────────────────
show("11) КЛЮЧ КЕША — СОДЕРЖИМОЕ, А НЕ ВРЕМЯ ФАЙЛА")
h := filepath.Join(work, "h")
copyDir(filepath.Join(cases, "hello"), h)
os.WriteFile(filepath.Join(h, "go.mod"), []byte("module h\n\ngo 1.24\n"), 0o644)
build := func() (int, int) {
return count(goRun(h, nil, "build", "-x", "-o", filepath.Join(work, "h.bin"), ".").out)
}
build()
c, l := build()
fmt.Printf(" повторная сборка без изменений compile: %d, link: %d\n", c, l)
later := time.Now().Add(time.Hour)
os.Chtimes(filepath.Join(h, "main.go"), later, later)
c, l = build()
fmt.Printf(" после touch main.go (время +1 ч) compile: %d, link: %d\n", c, l)
f, _ := os.OpenFile(filepath.Join(h, "main.go"), os.O_APPEND|os.O_WRONLY, 0)
f.WriteString("\n// комментарий\n")
f.Close()
c, l = build()
fmt.Printf(" после правки комментария compile: %d, link: %d\n", c, l)
fmt.Println(" Время изменения файла в ключ не входит, содержимое — входит:")
fmt.Println(" правка комментария перекомпилировала один пакет main и")
fmt.Println(" заново скомпоновала бинарник.")
}