Deep Engineering

MEASUREMENT

bench/goclosure/example.sh

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/closures
How to run it
go run bench/goclosure/internals.go
bash bench/goclosure/loopvar.sh
bash bench/goclosure/escape.sh
bash bench/goclosure/compile.sh
bash bench/goclosure/example.sh
go run bench/goclosure/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 пять наблюдений: функция как значение (тип, nil, размер, паника при вызове nil и при сравнении через интерфейс); замыкание захватывает переменную (счётчик-генератор, два замыкания над одной переменной, присваивание после создания замыкания); что вычисляется сразу (аргументы defer, получатель значения метода) и что потом (тело замыкания); вариативный параметр — f(s...) против f(a, b, c); выделения памяти на вызов
loopvar.sh запускает один и тот же main.go в двух модулях — cases/loop121 (go 1.21 в go.mod) и cases/loop122 (go 1.22) — и печатает решение компилятора -m=2 о захвате переменной цикла; раздел 3 (добавлен после аудита 14.10.2026) — assign.go: for _, s = range с заранее объявленной s
example.sh полный короткий пример урока: cases/example/main.go со своим go.mod, запуск go run . (добавлен после аудита 14.10.2026)
escape.sh собирает cases/escape с -gcflags='-m -l' и -m=2: moved to heap: n, func literal escapes to heap, func literal does not escape, захват по ссылке и по значению
compile.sh собирает cases/funccmp и печатает ошибки компилятора: f == g и карта с ключом-функцией
cases/*/ подопытные программы к скриптам, у каждой свой go.mod; funccmp намеренно не собирается; в loop121 и loop122 кроме main.go лежит assign.go — отдельным файлом, чтобы номера строк main.go, на которые ссылается урок, не сдвинулись
practice.go источник ответов двух практических задач урока: вывод программы со счётчиком, f(s...) и значением метода; цена вызова и создания замыкания против прямого вызова
go run bench/goclosure/internals.go
bash bench/goclosure/loopvar.sh
bash bench/goclosure/escape.sh
bash bench/goclosure/compile.sh
bash bench/goclosure/example.sh
go run bench/goclosure/practice.go

Записи прогонов — в runs/, go1.24.7 linux/amd64, 2 ядра. internals.go, loopvar.sh, escape.sh, compile.sh и example.sh запускались и на go1.25.1: вывод тот же с точностью до номера версии в первой строке, поэтому отдельных записей для них нет. Время в practice.go — лучший из 11 кругов testing.Benchmark на машине, которую делили с другими задачами; это одна конфигурация (linux/amd64, 2 ядра, //go:noinline, глобальный приёмник результата), и ни наносекунды, ни кратности не обещаны для другой машины или версии. Строка «устойчивы кратности, а не наносекунды» в runs/practice.txt — вывод прогона до аудита 14.10.2026; замер не перезапускался, чтобы не загружать общую машину, а урок эту формулировку больше не повторяет. Числа выделений — наблюдение компилятора go1.24.7/go1.25.1 в конкретном месте вызова, а не контракт языка (раздел 6 internals.go).

Что получилось

Функция — значение. %T у op — func(int) int, unsafe.Sizeof(op) — 8 байт. Нулевое значение — nil; вызов nf(1) — паника runtime error: invalid memory address or nil pointer dereference. f == g не собирается: func can only be compared to nil; сравнение через интерфейс — паника comparing uncomparable type func(int) int.

Замыкание захватывает переменную. c1() трижды — 1 2 3, новый counter() начинает с 1. Два замыкания из pair() делят одну переменную: после двух incr() get() даёт 2. Замыкание, созданное до x = 2, читает 2.

Что вычисляется сразу. defer record(x) при выходе видит x = 1 (аргумент вычислен в момент defer), defer func() { record(x) }() — x = 2. Значение метода v.Get с получателем-значением сохранило копию: после v.n = 2 оно возвращает 1. С получателем-указателем (p.PGet) — 2; а p.Get через указатель копирует *p и возвращает 1.

Переменная цикла — строка go в go.mod, если цикл объявляет её сам. Один и тот же main.go, один компилятор: с go 1.21 три замыкания из цикла по срезу дают c c c, пример из спецификации — 6 6 6, у &i за три итерации один адрес; с go 1.22 — a b c, 1 3 5, три адреса. Значение, переданное параметром, даёт a b c в обоих модулях, а for _, s = range с заранее объявленной s — c c c в обоих (раздел 3). Выделений на цикл из трёх сохранённых замыканий: 4 и 3 — в старом модуле s захвачена по ссылке и уходит в кучу (moved to heap: s), в новом захвачена по значению.

Вариативный параметр. bump(s...) записал 100 в s[0] вызывающего, first(s...) == &s[0] — true; bump(a1, 2, 3) переменную a1 не тронул. append внутри функции при запасе ёмкости записал 9 в массив вызывающего: s2[:3] = [0 0 9] при len(s2) = 2.

Выделения. Замыкание, вызванное на месте, — 0; counter() — 2 (переменная n и замыкание); fixed(), чья x не меняется, — 1, потому что x захвачена по значению (fixed capturing by value: x); сохранённое значение метода — 1; sum(1, 2, 3) и sum(s...) — 0. Раздел 6: тот же counter(), встроенный и использованный на месте, — 0 выделений; сохранённый в глобальную переменную — 2; не встраиваемая копия counterNI(), даже использованная на месте, — 2.

Цена. Вызов замыкания через переменную — 1,73 нс против 1,63 нс прямого вызова, кратность 1,06: в пределах шума. Создать замыкание, которое уходит наружу, и вызвать его — 20,82 нс и одно выделение, в 12,8 раза дороже прямого вызова. Кратности печатаются с двумя знаками, а отношение «создать и вызвать / прямой» — ещё и округлённым до одного знака: это ответ задачи Estimate.

Script

20 lines
#!/usr/bin/env bash
#
# Полный короткий пример урока: файл cases/example/main.go со своим go.mod
# (go 1.24), который можно скопировать в чистый каталог и запустить
# командой go run . — без зависимостей от остальных файлов bench/goclosure.
#
# ЗАЧЕМ. Добавлен после внешнего аудита 14.10.2026: остальные программы
# каталога — стенды наблюдений, их вывод не заменяет запускаемого примера.
#
# ЗАПУСК:
#
#	bash bench/goclosure/example.sh
set -u

cd "$(dirname "$0")/cases/example"

echo "$(go env GOVERSION) $(go env GOOS)/$(go env GOARCH) | полный пример: go run ."
echo
go run .