MEASUREMENT
bench/goclosure/compile.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
26 lines#!/usr/bin/env bash
#
# Функции сравнимы только с nil — ошибка компилятора.
#
# ЗАЧЕМ ОТДЕЛЬНЫЙ СКРИПТ. Программа с этой ошибкой не собирается, поэтому
# показать её из internals.go нельзя. Здесь собирается cases/funccmp и
# печатается то, что ответил компилятор, — без строки с именем модуля.
#
# В cases/funccmp три выражения: f == nil (законно), f == g и карта с
# ключом типа func(int) int (обе строки — ошибки).
#
# ЗАПУСК:
#
# bash bench/goclosure/compile.sh
set -u
cd "$(dirname "$0")/cases"
echo "$(go env GOVERSION) $(go env GOOS)/$(go env GOARCH) | ошибки компилятора: сравнение функций"
for d in funccmp; do
echo
echo "$d:"
(cd "$d" && go build -o /dev/null . 2>&1) | grep -v '^#' | sed 's/^/ /'
done