ЗАМЕР
bench/gotest/boundaries.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, а не лучший прогон.
Скрипт
104 строк//go:build ignore
// Границы тестов: уточнения к internals.go после аудита 07.10.2026.
//
// ЗАЧЕМ ОТДЕЛЬНАЯ ПРОГРАММА. internals.go меряет то, что урок цитирует
// числами, и его перезапуск менял бы их. Здесь — то, что добавлено позже.
//
// ПЯТЬ СЮЖЕТОВ:
//
// 1. Ещё два способа закрыть ресурс параллельных подтестов: код после группы
// t.Run и defer внутри самого подтеста.
// 2. t.Setenv в параллельном тесте — паника пакета testing.
// 3. t.Fatal из чужой горутины останавливает её, а не тест; go vet это ловит.
// 4. testing/synctest в go1.24.7 (эксперимент): сутки сна в пузыре — без
// ожидания.
// 5. Разброс бенчмарка: десять прогонов вместо «лучшего из пяти».
//
// ЗАПУСК:
//
// go run bench/gotest/boundaries.go
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
)
var here string
func goCmd(env []string, args ...string) string {
cmd := exec.Command("go", args...)
cmd.Dir = here
cmd.Env = append(os.Environ(), env...)
out, _ := cmd.CombinedOutput()
return string(out)
}
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
func lines(out string, keep func(string) bool) {
for _, l := range strings.Split(strings.TrimRight(out, "\n"), "\n") {
if keep(l) {
fmt.Println(" " + strings.TrimSpace(l))
}
}
}
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) ГРУППА t.Run И defer ВНУТРИ ПОДТЕСТА")
lines(goCmd(nil, "test", "-count=1", "-v", "./cases/group/"), event)
show("2) t.Setenv В ПАРАЛЛЕЛЬНОМ ТЕСТЕ")
lines(goCmd(nil, "test", "-count=1", "./cases/setenv/"), func(l string) bool {
return strings.HasPrefix(l, "panic: testing:") || strings.HasPrefix(l, "--- FAIL")
})
show("3) t.Fatal ИЗ ЧУЖОЙ ГОРУТИНЫ")
lines(goCmd(nil, "test", "-count=1", "-v", "./cases/fatalgo/"), func(l string) bool {
return event(l) || strings.Contains(l, "ошибка в горутине") || strings.HasPrefix(l, "--- FAIL")
})
fmt.Println(" go vet:")
lines(goCmd(nil, "vet", "./cases/fatalgo/"), func(l string) bool { return strings.Contains(l, "goroutine") })
show("4) testing/synctest (GOEXPERIMENT=synctest)")
lines(goCmd([]string{"GOEXPERIMENT=synctest"}, "test", "-count=1", "-v", "./cases/synctest/"), event)
show("5) РАЗБРОС БЕНЧМАРКА: ДЕСЯТЬ ПРОГОНОВ")
out := goCmd(nil, "test", "-run", "^$", "-bench", "Discard|Sink", "-count=10", "./cases/bench/")
nsop := regexp.MustCompile(`^(Benchmark\w+)-\d+\s+\d+\s+([\d.]+) ns/op`)
runs := map[string][]float64{}
for _, l := range strings.Split(out, "\n") {
if m := nsop.FindStringSubmatch(l); m != nil {
v, _ := strconv.ParseFloat(m[2], 64)
runs[m[1]] = append(runs[m[1]], v)
}
}
fmt.Printf(" %-18s %8s %8s %8s\n", "", "мин", "медиана", "макс")
for _, name := range []string{"BenchmarkDiscard", "BenchmarkSink"} {
r := runs[name]
sort.Float64s(r)
med := (r[len(r)/2-1] + r[len(r)/2]) / 2
fmt.Printf(" %-18s %8.2f %8.2f %8.2f нс/оп, %d прогонов\n", name, r[0], med, r[len(r)-1], len(r))
}
fmt.Println(" «Лучший из N» отбрасывает разброс. Для выводов о скорости")
fmt.Println(" сравнивают распределения серий — это делает benchstat.")
}