ЗАМЕР
bench/gotest/internals.go
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/golang/testing
- Как запустить
go run bench/gotest/internals.go go run bench/gotest/practice.go go run bench/gotest/boundaries.go
Запись прогона
Замеры для урока «Тестирование»
| Файл | Что делает |
|---|---|
internals.go |
запускает go test на пакетах из cases/ и legacy/ и печатает шесть наблюдений: подтесты табличного теста, порядок defer и t.Cleanup при параллельных подтестах, переменная цикла в модулях с go 1.21 и go 1.24, предел -parallel, кеш результатов, бенчмарк с выброшенным результатом |
boundaries.go |
добавлен после аудита 07.10.2026, чтобы не перезапускать internals.go с его числами: группа t.Run и defer внутри подтеста, t.Setenv в параллельном тесте, t.Fatal из чужой горутины и go vet, testing/synctest (в go1.24.7 — эксперимент, GOEXPERIMENT=synctest программа ставит сама), разброс бенчмарка за десять прогонов |
practice.go |
источник ответов двух практических задач урока; тест первой задачи запускает настоящим пакетом testing через testing.Main |
cases/table |
функция Clamp и её табличный тест |
cases/order |
defer против t.Cleanup при параллельных подтестах |
cases/loopvar |
переменная цикла в параллельных подтестах, модуль с go 1.24 |
legacy/loopvar |
тот же тест в отдельном модуле, чей go.mod объявляет go 1.21 |
cases/parallel |
подтесты со сном: сколько из них идёт одновременно |
cases/group |
группа t.Run как барьер для параллельных подтестов и defer внутри подтеста |
cases/setenv |
t.Setenv вместе с t.Parallel |
cases/fatalgo |
t.Fatal из горутины, запущенной тестом |
cases/synctest |
сутки сна в «пузыре» synctest.Run; собирается только с GOEXPERIMENT=synctest |
cases/bench |
три способа написать один замер: результат выброшен, сохранён, b.Loop |
go run bench/gotest/internals.go
go run bench/gotest/practice.go
go run bench/gotest/boundaries.go
Все три программы сами вызывают go test, поэтому нужен установленный Go.
Раздел 5 internals.go выполняет go clean -testcache: кеш результатов
тестов на машине очищается. Записи прогонов — в runs/, go1.24.7
linux/amd64, GOMAXPROCS=2.
Что получилось
Табличный тест — это подтесты. Каждый случай таблицы запускается через
t.Run под своим именем, и -run 'TestClamp/выше' запускает его один.
defer в тесте с параллельными подтестами срабатывает слишком рано.
Подтест с t.Parallel() ждёт, пока тело родителя закончится, а defer
срабатывает именно тогда: оба подтеста увидели уже закрытый ресурс. С
t.Cleanup — открытый: он вызывается после всех подтестов.
Переменная цикла зависит от go.mod, а не от компилятора. Один и тот же
код, собранный go1.24.7: в модуле с go 1.21 все три параллельных подтеста
видят последний случай (tc = c), в модуле с go 1.24 — каждый свой. go vet
в старом модуле предупреждает: loop variable tc captured by func literal.
-parallel ограничивает число подтестов, а не загрузку ядер. Четыре
подтеста по 200 мс сна: 404 мс по умолчанию (-parallel = GOMAXPROCS = 2),
805 мс с -parallel=1, 203 мс с -parallel=4.
Кеш. Повторный go test печатает (cached) и тестов не запускает;
-count=1 — идиоматичный способ кеш обойти.
Бенчмарк выброшенного результата меряет пустой цикл. Лучший из пяти:
0,32 нс против 1,65 нс, когда результат сохранён; b.Loop — 2,20 нс: внутри
него компилятор вызов не встраивает (skip inlining within testing.B.loop).
Наносекунды не переносятся; переносится то, что пустой цикл в разы быстрее.
После аудита 07.10.2026 (boundaries.go)
Группа — второй барьер. Код после t.Run("group", …) выполняется, когда
параллельные подтесты группы закончились: оба видели ресурс открытым. defer
внутри подтеста срабатывает после работы этого подтеста — для своего ресурса
подтеста он годится.
t.Setenv с t.Parallel — паника пакета: test using t.Setenv or t.Chdir can not use t.Parallel.
t.Fatal из чужой горутины останавливает её, а не тест. Тест помечен
упавшим, но код после ожидания горутины выполнился; go vet сообщает
call to (*testing.T).Fatal from a non-test goroutine.
testing/synctest (go1.24.7, эксперимент). Сутки сна внутри пузыря —
24h0m0s по часам пузыря и меньше секунды по настоящим.
Разброс. Десять прогонов: BenchmarkDiscard 0,33–0,42 нс (медиана 0,34),
BenchmarkSink 1,61–1,95 нс (медиана 1,76). Для выводов «стало быстрее»
сравнивают серии через benchstat, а не лучший прогон.
Скрипт
185 строк//go:build ignore
// Тестирование в Go: что на самом деле делают t.Run, t.Parallel, кеш
// результатов и бенчмарк.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Вопросы о тестах на собеседовании звучат как вопросы о
// синтаксисе — «что такое табличный тест», «зачем t.Parallel». А ответы,
// которые отличают знающего, — о поведении: в каком порядке что выполняется,
// что кешируется, что именно замерил бенчмарк. Здесь каждое такое поведение
// получено запуском `go test`, а не пересказано.
//
// ШЕСТЬ СЮЖЕТОВ:
//
// 1. Табличный тест: каждый случай — подтест со своим именем, и его можно
// запустить отдельно.
// 2. Параллельные подтесты и defer: defer родителя срабатывает раньше, чем
// параллельные подтесты начнут работу; t.Cleanup — после них.
// 3. Переменная цикла: в модуле с go 1.21 все параллельные подтесты видят
// последний случай таблицы, с go 1.22+ — каждый свой.
// 4. Сколько подтестов идёт одновременно: не больше -parallel, а он по
// умолчанию равен GOMAXPROCS — даже если подтесты спят.
// 5. Кеш результатов: повторный go test не запускает тесты, а печатает
// сохранённый результат.
// 6. Бенчмарк: результат, который никто не использует, компилятор
// выбрасывает вместе с вызовом.
//
// ЗАПУСК:
//
// go run bench/gotest/internals.go
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
"time"
)
var here string
func goCmd(dir string, args ...string) string {
cmd := exec.Command("go", args...)
cmd.Dir = dir
out, _ := cmd.CombinedOutput()
return string(out)
}
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
var timing = regexp.MustCompile(`\t\d+\.\d+s$|\(\d+\.\d+s\)`)
func pick(out string, keep func(string) bool) {
for _, l := range strings.Split(strings.TrimRight(out, "\n"), "\n") {
if keep(l) {
fmt.Println(" " + strings.TrimRight(timing.ReplaceAllString(l, ""), " \t"))
}
}
}
func event(l string) bool { return strings.HasPrefix(l, "EVENT ") }
func main() {
_, file, _, _ := runtime.Caller(0)
here = filepath.Dir(file)
fmt.Printf("%s %s/%s, GOMAXPROCS=%d | тестирование\n", runtime.Version(), runtime.GOOS, runtime.GOARCH, runtime.GOMAXPROCS(0))
show("1) ТАБЛИЧНЫЙ ТЕСТ: КАЖДЫЙ СЛУЧАЙ — ПОДТЕСТ")
fmt.Println(" go test -v ./cases/table/")
pick(goCmd(here, "test", "-count=1", "-v", "./cases/table/"), func(l string) bool {
return strings.HasPrefix(strings.TrimSpace(l), "--- PASS")
})
fmt.Println(" go test -v -run 'TestClamp/выше' ./cases/table/")
pick(goCmd(here, "test", "-count=1", "-v", "-run", "TestClamp/выше", "./cases/table/"), func(l string) bool {
return strings.HasPrefix(strings.TrimSpace(l), "--- PASS")
})
show("2) ПАРАЛЛЕЛЬНЫЕ ПОДТЕСТЫ: defer ПРОТИВ t.Cleanup")
pick(goCmd(here, "test", "-count=1", "-v", "./cases/order/"), event)
fmt.Println(" Подтест с t.Parallel() ждёт, пока тело родителя закончится.")
fmt.Println(" defer срабатывает в этот момент — до подтестов; t.Cleanup —")
fmt.Println(" после того, как закончились все подтесты.")
show("3) ПЕРЕМЕННАЯ ЦИКЛА В ПАРАЛЛЕЛЬНЫХ ПОДТЕСТАХ")
fmt.Println(" модуль с go 1.21 в go.mod:")
legacy := goCmd(filepath.Join(here, "legacy"), "test", "-count=1", "-v", "./loopvar/")
ev := []string{}
for _, l := range strings.Split(legacy, "\n") {
if event(l) {
ev = append(ev, l)
}
}
sort.Strings(ev)
pick(strings.Join(ev, "\n"), event)
pick(legacy, func(l string) bool { return strings.HasPrefix(l, "ok") || strings.HasPrefix(l, "FAIL") })
fmt.Println(" модуль с go 1.24 в go.mod — тот же код:")
ev = ev[:0]
for _, l := range strings.Split(goCmd(here, "test", "-count=1", "-v", "./cases/loopvar/"), "\n") {
if event(l) {
ev = append(ev, l)
}
}
sort.Strings(ev)
pick(strings.Join(ev, "\n"), event)
fmt.Println(" go vet в модуле с go 1.21:")
pick(goCmd(filepath.Join(here, "legacy"), "vet", "./loopvar/"), func(l string) bool {
return strings.Contains(l, "captured")
})
show("4) СКОЛЬКО ПОДТЕСТОВ ИДЁТ ОДНОВРЕМЕННО")
fmt.Println(" четыре подтеста по 200 мс сна, все с t.Parallel()")
// Тестовый бинарник собирается один раз и запускается напрямую: так в
// замер не попадает время сборки, а только время самих тестов.
bin := filepath.Join(os.TempDir(), "gotest-parallel.test")
goCmd(here, "test", "-c", "-o", bin, "./cases/parallel/")
for _, p := range []string{"", "1", "4"} {
args := []string{"-test.run", "TestSleep"}
label := "-parallel по умолчанию (= GOMAXPROCS)"
if p != "" {
args = append(args, "-test.parallel="+p)
label = "-parallel=" + p
}
lo := time.Duration(1 << 62)
for i := 0; i < 3; i++ {
start := time.Now()
if out, err := exec.Command(bin, args...).CombinedOutput(); err != nil {
panic(fmt.Sprintf("%v\n%s", err, out))
}
if d := time.Since(start); d < lo {
lo = d
}
}
fmt.Printf(" %-40s %4.0f мс\n", label, float64(lo.Microseconds())/1000)
}
fmt.Println(" Лучший из 3 запусков. Сон процессор не занимает, но флаг")
fmt.Println(" -parallel ограничивает число подтестов, а не загрузку ядер.")
show("5) КЕШ РЕЗУЛЬТАТОВ")
goCmd(here, "clean", "-testcache")
for _, args := range [][]string{
{"test", "./cases/table/"},
{"test", "./cases/table/"},
{"test", "-count=1", "./cases/table/"},
} {
fmt.Printf(" go %s\n", strings.Join(args, " "))
pick(goCmd(here, args...), func(l string) bool { return strings.HasPrefix(l, "ok") })
}
show("6) БЕНЧМАРК: РЕЗУЛЬТАТ, КОТОРЫЙ НИКТО НЕ ИСПОЛЬЗУЕТ")
out := goCmd(here, "test", "-run", "^$", "-bench", ".", "-count=5", "./cases/bench/")
best := map[string]float64{}
nsop := regexp.MustCompile(`^(Benchmark\w+)-\d+\s+\d+\s+([\d.]+) ns/op`)
for _, l := range strings.Split(out, "\n") {
if m := nsop.FindStringSubmatch(l); m != nil {
v, _ := strconv.ParseFloat(m[2], 64)
if old, ok := best[m[1]]; !ok || v < old {
best[m[1]] = v
}
}
}
for _, name := range []string{"BenchmarkDiscard", "BenchmarkSink", "BenchmarkLoop"} {
fmt.Printf(" %-18s %6.2f нс/оп\n", name, best[name])
}
fmt.Printf(" Sink / Discard: %.1f\n", best["BenchmarkSink"]/best["BenchmarkDiscard"])
fmt.Println(" Лучший из 5 прогонов. Что решил компилятор (-gcflags=-m):")
// Путь к файлу печатается без каталога: строки вида «каталог/файл:строка»
// индекс замеров сайта принял бы за ссылку на скрипт.
gcm := goCmd(here, "test", "-run", "^$", "-bench", "Loop", "-benchtime=1x", "-gcflags=-m", "./cases/bench/")
pick(strings.ReplaceAll(gcm, "cases/bench/", ""), func(l string) bool {
return strings.Contains(l, "inlining call to work") || strings.Contains(l, "skip inlining within testing.B.loop")
})
fmt.Println(" В Discard вызов work встроен, а его результат никому не нужен —")
fmt.Println(" остаётся пустой цикл. b.Loop не даёт выбросить тело цикла:")
fmt.Println(" внутри него компилятор вызов не встраивает.")
}