ЗАМЕР
bench/goclosure/internals.go
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/golang/closures
- Как запустить
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
Запись прогона
Замеры для урока «Функции и замыкания в 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.
Скрипт
270 строк//go:build ignore
// Функции и замыкания в Go: что захватывается и когда.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. На собеседовании про замыкания обычно звучит одна
// фраза — «замыкание захватывает переменные из окружения», — и почти все
// вопросы дальше проверяют, понял ли кандидат слово «переменные». Не
// значения: переменную, которую видят и меняют все, кто её захватил. Отсюда
// следуют счётчик-генератор, переменная цикла, уход в кучу — и обратные
// случаи, где значение всё-таки фиксируется в момент вычисления: аргументы
// defer, значение метода, параметр.
//
// ПЯТЬ СЮЖЕТОВ:
//
// 1. Функция — значение. У неё есть тип, нулевое значение nil, размер
// указателя; вызов nil — паника; сравнить две функции нельзя.
// 2. Замыкание захватывает переменную. Счётчик-генератор, два замыкания
// над одной переменной, изменение снаружи после создания замыкания.
// 3. Что вычисляется сразу. Аргументы defer — в момент defer, тело
// замыкания — при выходе. Значение метода x.M копирует получатель-
// значение в момент вычисления.
// 4. Вариативный параметр. f(s...) передаёт тот же срез, f(a, b, c)
// создаёт новый.
// 5. Выделения. Сколько стоит замыкание, вызванное на месте, возвращённое,
// значение метода и вариативный вызов — в выделениях памяти на вызов.
// 6. Добавлено после внешнего аудита 14.10.2026: число выделений у
// counter() зависит от того, где и как её вызвали. Встроенный вызов,
// чей результат не уходит наружу, и вызов не встраиваемой копии
// counter — разные числа для одного и того же исходного кода.
//
// Переменная цикла — в loopvar.sh (два модуля с разной строкой go в
// go.mod), решения компилятора — в escape.sh, ошибки компиляции — в
// compile.sh.
//
// ЗАПУСК:
//
// go run bench/goclosure/internals.go
package main
import (
"fmt"
"runtime"
"strings"
"testing"
"unsafe"
)
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
// try печатает результат или текст паники.
func try(label string, f func() string) {
defer func() {
if r := recover(); r != nil {
fmt.Printf(" %-34s -> паника: %v\n", label, r)
}
}()
fmt.Printf(" %-34s -> %s\n", label, f())
}
// -------------------------------------------------- 1. функция — значение
func inc(x int) int { return x + 1 }
func double(x int) int { return x * 2 }
func apply(f func(int) int, x int) int { return f(x) }
// ------------------------------------------ 2. захватывается переменная
func counter() func() int {
n := 0
return func() int {
n++
return n
}
}
// pair — два замыкания над одной переменной.
func pair() (incr func(), get func() int) {
n := 0
incr = func() { n++ }
get = func() int { return n }
return
}
// --------------------------------------------- 3. что вычисляется сразу
var log []string
func record(who string, x int) { log = append(log, fmt.Sprintf("%-32s -> x = %d", who, x)) }
func deferDemo() {
x := 1
defer record("defer record(x)", x)
defer func() { record("defer func() { record(x) }()", x) }()
x = 2
}
type Counter struct{ n int }
func (c Counter) Get() int { return c.n } // получатель — значение
func (c *Counter) PGet() int { return c.n } // получатель — указатель
// ---------------------------------------------- 4. вариативный параметр
func bump(xs ...int) {
if len(xs) > 0 {
xs[0] = 100
}
}
func isNil(xs ...int) bool { return xs == nil }
func first(xs ...int) *int { return &xs[0] }
func add9(xs ...int) []int { return append(xs, 9) }
// ------------------------------------------------------------ 5. выделения
var (
sinkF func() int
sinkInt int
gv = Counter{n: 7}
)
//go:noinline
func inPlace() int {
n := 0
f := func() { n++ }
f()
f()
return n
}
//go:noinline
func fixed() func() int {
x := 42
return func() int { return x }
}
//go:noinline
func sum(xs ...int) int {
s := 0
for _, x := range xs {
s += x
}
return s
}
// counterNI — тот же counter, но компилятору запрещено её встраивать.
//
//go:noinline
func counterNI() func() int {
n := 0
return func() int {
n++
return n
}
}
func main() {
fmt.Printf("%s %s/%s | функции и замыкания\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
show("1) ФУНКЦИЯ — ЗНАЧЕНИЕ")
var op func(int) int = inc
fmt.Printf(" var op func(int) int = inc -> op(41) = %d\n", op(41))
fmt.Printf(" apply(double, 5) -> %d\n", apply(double, 5))
fmt.Printf(" %%T op -> %T\n", op)
fmt.Printf(" unsafe.Sizeof(op) -> %d байт\n", unsafe.Sizeof(op))
var nf func(int) int
fmt.Printf(" var nf func(int) int; nf == nil -> %v\n", nf == nil)
try("nf(1)", func() string { return fmt.Sprint(nf(1)) })
f, g := any(inc), any(inc)
try("any(inc) == any(inc)", func() string { return fmt.Sprint(f == g) })
fmt.Println(" Функция — значение: её кладут в переменную, передают и")
fmt.Println(" возвращают. Нулевое значение — nil, и вызов nil — паника.")
fmt.Println(" f == g не собирается (compile.sh); через интерфейс — паника.")
show("2) ЗАМЫКАНИЕ ЗАХВАТЫВАЕТ ПЕРЕМЕННУЮ, А НЕ ЗНАЧЕНИЕ")
c1 := counter()
a, b, c := c1(), c1(), c1()
c2 := counter()
fmt.Printf(" c1 := counter(); c1() x3 -> %d %d %d\n", a, b, c)
fmt.Printf(" c2 := counter(); c2() -> %d\n", c2())
incr, get := pair()
incr()
incr()
fmt.Printf(" incr, get := pair(); incr() x2 -> get() = %d\n", get())
x := 1
read := func() int { return x }
x = 2
fmt.Printf(" x := 1; read := ...; x = 2 -> read() = %d\n", read())
set := func() { x = 10 }
set()
fmt.Printf(" set := func() { x = 10 }; set() -> x = %d\n", x)
fmt.Println(" У каждого вызова counter своя n; incr и get делят одну n;")
fmt.Println(" read видит присваивание, сделанное после её создания.")
show("3) ЧТО ВЫЧИСЛЯЕТСЯ СРАЗУ, А ЧТО ПОТОМ")
deferDemo()
fmt.Println(" x := 1; два defer; x = 2 — что увидели при выходе:")
for _, l := range log {
fmt.Println(" " + l)
}
v := Counter{n: 1}
getV := v.Get
v.n = 2
fmt.Printf(" v := Counter{1}; g := v.Get; v.n = 2 -> g() = %d, v.Get() = %d\n", getV(), v.Get())
p := &Counter{n: 1}
pget := p.PGet
pgetV := p.Get
p.n = 2
fmt.Printf(" p := &Counter{1}; g := p.PGet; p.n = 2 -> g() = %d (PGet на *Counter)\n", pget())
fmt.Printf(" p := &Counter{1}; g := p.Get; p.n = 2 -> g() = %d (Get на Counter)\n", pgetV())
fmt.Println(" Аргументы defer и получатель значения метода вычисляются")
fmt.Println(" сразу и сохраняются копией. Тело замыкания читает переменную")
fmt.Println(" тогда, когда выполняется.")
show("4) ВАРИАТИВНЫЙ ПАРАМЕТР: ТОТ ЖЕ СРЕЗ ИЛИ НОВЫЙ")
s := []int{1, 2, 3}
bump(s...)
fmt.Printf(" s := []int{1, 2, 3}; bump(s...) -> s = %v\n", s)
a1 := 1
bump(a1, 2, 3)
fmt.Printf(" a1 := 1; bump(a1, 2, 3) -> a1 = %d\n", a1)
fmt.Printf(" first(s...) == &s[0] -> %v\n", first(s...) == &s[0])
fmt.Printf(" isNil() -> %v\n", isNil())
s2 := make([]int, 2, 4)
r := add9(s2...)
fmt.Printf(" s2 := make([]int, 2, 4); add9(s2...) -> s2[:3] = %v, len(s2) = %d\n", s2[:3], len(s2))
_ = r
fmt.Println(" f(s...) передаёт сам срез: запись в xs[0] видна снаружи, и")
fmt.Println(" append с запасом ёмкости пишет в массив вызывающего.")
fmt.Println(" f(a, b, c) собирает новый срез из копий аргументов.")
show("5) ВЫДЕЛЕНИЯ ПАМЯТИ НА ВЫЗОВ")
allocs := func(label string, f func()) {
fmt.Printf(" %-44s %.0f\n", label, testing.AllocsPerRun(1000, f))
}
allocs("замыкание вызвано на месте (inPlace)", func() { sinkInt = inPlace() })
allocs("counter(): n меняется, замыкание возвращено", func() { sinkF = counter() })
allocs("fixed(): x не меняется, замыкание возвращено", func() { sinkF = fixed() })
allocs("значение метода gv.Get сохранено", func() { sinkF = gv.Get })
allocs("sum(1, 2, 3)", func() { sinkInt = sum(1, 2, 3) })
allocs("sum(s...)", func() { sinkInt = sum(s...) })
fmt.Println(" counter — две: переменная n и само замыкание. fixed — одна:")
fmt.Println(" x никто не меняет, и она копируется в замыкание (escape.sh).")
show("6) ВЫДЕЛЕНИЯ У counter() ЗАВИСЯТ ОТ МЕСТА ВЫЗОВА")
allocs("c := counter(); c(); c() — на месте", func() {
c := counter()
c()
sinkInt = c()
})
allocs("sinkF = counter() — сохранено наружу", func() { sinkF = counter() })
allocs("c := counterNI(); c(); c() — noinline", func() {
c := counterNI()
c()
sinkInt = c()
})
fmt.Println(" Исходный код counter один и тот же. Встроенный вызов, чьё")
fmt.Println(" замыкание не уходит наружу, обошёлся без кучи; сохранённый")
fmt.Println(" наружу результат и не встраиваемая копия — по два выделения.")
fmt.Println(" Число выделений — наблюдение этой версии компилятора в этом")
fmt.Println(" месте вызова, а не свойство функции.")
}