MEASUREMENT
bench/goptr/audit.go
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/pointers
- How to run it
go run bench/goptr/internals.go bash bench/goptr/compile.sh bash bench/goptr/escape.sh bash bench/goptr/inline.sh bash bench/goptr/example.sh go run bench/goptr/audit.go go run bench/goptr/nilcheck.go /usr/local/go1.25.1/bin/go run bench/goptr/nilcheck.go go run bench/goptr/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 |
девять наблюдений: & и *, p.X и вызов метода без звёздочки; параметр-указатель как копия (изменение через него видно, присваивание ему — нет); nil-указатель: паника при разыменовании и законный вызов метода на nil-получателе; new(T) и &T{}; адрес элемента среза до и после append, который перевыделил массив; указатель на локальную переменную и число выделений; адресная арифметика через unsafe.Add; срез и карта в параметре; указатель на указатель. Адреса печатаются метками <адрес N> |
compile.sh |
собирает cases/{mapaddr,mapfield,ptrarith,addrconst,newexpr} и печатает ошибки компилятора: адрес элемента карты, присваивание полю структуры в карте, p + 1 и p++, &10 и &get(), new(42) (на go1.24.7 и go1.25.1 не собирается) |
escape.sh |
собирает cases/escape с -gcflags='-m -l': moved to heap: c для возвращённого &c, одинаковые решения для new(Point) и &Point{} |
audit.go |
три наблюдения, добавленные после внешнего аудита 14.10.2026: new(T) и &T{} для структуры, среза и карты (в том числе запись в карту); указатель на элемент после перевыделения и удержание старого массива (живая куча после GC); число выделений у встраиваемой и невстраиваемой функции, возвращающей &c |
inline.sh |
собирает cases/inline с -gcflags='-m' без -l: одна функция, moved to heap только в том месте вызова, где указатель уходит наружу |
example.sh |
запускает полный короткий пример урока cases/example (отдельный модуль example) |
cases/*/main.go |
подопытные программы к compile.sh, escape.sh, inline.sh и example.sh, у каждой свой go.mod; mapaddr, mapfield, ptrarith, addrconst и newexpr намеренно не собираются |
nilcheck.go |
разыменование nil, чей результат на пройденном пути не нужен: go1.24.7 проверку переносит в другую ветку и пропускает, go1.25.1 — паникует (тест test/fixedbugs/issue72860.go и исключение в ssa/tighten.go, строки 48–49, дистрибутива go1.25.1) |
practice.go |
источник ответов двух практических задач урока: предсказание вывода и кратность newBig / makeBig (возврат 1 КБ указателем и значением); рядом — цена передачи 16 Б и 1 КБ по значению и по указателю для раздела «Глубже» |
go run bench/goptr/internals.go
bash bench/goptr/compile.sh
bash bench/goptr/escape.sh
bash bench/goptr/inline.sh
bash bench/goptr/example.sh
go run bench/goptr/audit.go
go run bench/goptr/nilcheck.go
/usr/local/go1.25.1/bin/go run bench/goptr/nilcheck.go
go run bench/goptr/practice.go
Записи прогонов — в runs/, go1.24.7 linux/amd64, 2 ядра. nilcheck.go
записан и на go1.25.1 (runs/nilcheck-go1.25.txt). internals.go,
compile.sh, escape.sh, inline.sh, example.sh и audit.go на go1.25.1
дают тот же вывод с точностью до номера версии в первой строке, поэтому
отдельных записей для них нет. Новее go1.25.1 урок не измерял (сверено
14.10.2026).
Что получилось
Параметр-указатель — копия. Снаружи p = <адрес 1>, &p = <адрес 2>;
внутри функции p = <адрес 1>, &p = <адрес 3>: значение то же, переменная
другая. Поэтому *p = 42 внутри видно снаружи (x = 42), а p = &y и
p = nil — нет. Срез и карта ведут себя так же: запись в элемент видна,
append и m = nil — нет.
nil-указатель. *np, np.X и np.Sum() (метод на Point) — паника
runtime error: invalid memory address or nil pointer dereference, значение
паники — runtime.Error. Метод на *Tree с nil-получателем работает: Kind
полей не трогает и вернул "tree", Sum проверяет nil и вернул 0.
new(T) и &T{} для структуры — одно и то же, для среза и карты — нет.
Для Point оба дают нулевую структуру, по одному выделению, если указатель
уходит в глобальную переменную, и одинаковые решения -m. Но *new([]int) и
*new(map[string]int) — nil, а *(&[]int{}) и *(&map[string]int{}) — нет;
запись в карту из new — паника assignment to entry in nil map
(audit.go, раздел 1).
Адрес элемента среза устаревает. После append в срез с cap 3 массив
перевыделен (cap 6, e == &s[0] — false), и *e = 100 пишет в старый
массив: s[0] = 10, *e = 100. Указатель при этом не испорчен — читать и
писать через него законно, — но он больше не связан с текущим массивом среза и
удерживает старый в памяти: живая куча 18 МБ, пока жив e, и 10 МБ без него
(audit.go, раздел 2). С запасом ёмкости связь сохраняется.
Адрес элемента карты не берётся вовсе: cannot take address of m["a"].
Указатель на локальную переменную законно вернуть. Число выделений — не
свойство функции, а наблюдение в конкретном месте вызова: newCounter() с
//go:noinline — одно выделение на вызов и moved to heap: c; встраиваемая
копия, чей указатель не ушёл наружу, — ноль, а при записи результата в
глобальную переменную — одно (audit.go, раздел 3; inline.sh).
Арифметики нет. p + 1 — mismatched types *int and untyped int, p++ —
non-numeric type *int. Через unsafe.Add(unsafe.Pointer(q), 4) читается
arr[1] = 20.
Проверка на nil в go1.24.7 может не выполниться. f(nil, false) из
issue72860.go вернул 5, а код, читающий file.Name() до проверки ошибки,
отработал без паники: проверку на nil компилятор перенёс в ветку, где значение
используется. На go1.25.1 оба случая — паника.
Цена. Лучший из 11 кругов testing.Benchmark, функции с //go:noinline,
результат пишется в глобальную переменную. От запуска к запуску кратности
заметно меняются; это наблюдение в одной конфигурации, на другой машине и
версии их надо мерить заново. Передача 16 Б по значению
и по указателю — 1,66 и 1,56 нс; 1 КБ — 23,01 против 1,65 нс, кратность 13,92.
Но newBig(), который возвращает *Big, — 301,79 нс, одно выделение на 1024
байта, а makeBig(), который возвращает Big значением, — 25,62 нс без
выделений: указатель здесь дороже в 11,8 раза.
Script
126 lines//go:build ignore
// Указатели: три наблюдения, добавленные после внешнего аудита 14.10.2026.
//
// ЗАЧЕМ ОТДЕЛЬНАЯ ПРОГРАММА. internals.go цитируется в уроке блоками, и
// менять его вывод значит ломать цитаты. Здесь — то, чего в нём не хватало:
//
// 1. new(T) и &T{} одинаковы для структуры, но не для среза и карты:
// new даёт указатель на НУЛЕВОЕ значение (nil-срез, nil-карта), а &T{} —
// на пустой, но инициализированный контейнер. В nil-карту писать нельзя.
// 2. Указатель на элемент после перевыделения массива не «испорчен»: читать
// и писать через него законно, он просто больше не связан с текущим
// массивом среза — и удерживает старый массив в памяти.
// 3. Число выделений у функции, возвращающей адрес своей переменной, зависит
// от места вызова: встроенная копия, чей указатель не ушёл наружу,
// обходится без кучи. Число выделений — наблюдение реализации, а не
// правило языка.
//
// Числа в мегабайтах округлены вниз до целого; HeapAlloc снят сразу после
// runtime.GC().
//
// ЗАПУСК:
//
// go run bench/goptr/audit.go
package main
import (
"fmt"
"runtime"
"strings"
"testing"
)
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
func panicText(f func()) (msg string) {
defer func() {
if r := recover(); r != nil {
msg = fmt.Sprint(r)
}
}()
f()
return ""
}
type Point struct{ X, Y int }
// newCounterInl может быть встроена: //go:noinline нет.
func newCounterInl() *int {
c := 0
return &c
}
// newCounter не встраивается — как в internals.go.
//
//go:noinline
func newCounter() *int {
c := 0
return &c
}
var sinkP *int
var sinkN int
func heapMB() uint64 {
runtime.GC()
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
return ms.HeapAlloc >> 20
}
func main() {
fmt.Printf("%s %s/%s | указатели: дополнения после аудита\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
show("1) new(T) И &T{} ДЛЯ СТРУКТУРЫ, СРЕЗА И КАРТЫ")
fmt.Println(" T *new(T) *(&T{})")
fmt.Printf(" Point %-20s %s\n", fmt.Sprintf("%+v", *new(Point)), fmt.Sprintf("%+v", *(&Point{})))
fmt.Printf(" []int == nil: %-12v == nil: %v\n", *new([]int) == nil, *(&[]int{}) == nil)
fmt.Printf(" map[string]int == nil: %-12v == nil: %v\n", *new(map[string]int) == nil, *(&map[string]int{}) == nil)
fmt.Println()
fmt.Println(" запись (*m)[\"k\"] = 1:")
mn := new(map[string]int)
fmt.Printf(" m := new(map[string]int) -> паника: %s\n", panicText(func() { (*mn)["k"] = 1 }))
ml := &map[string]int{}
(*ml)["k"] = 1
fmt.Printf(" m := &map[string]int{} -> len %d\n", len(*ml))
fmt.Println(" *s = append(*s, 1):")
sn := new([]int)
*sn = append(*sn, 1)
fmt.Printf(" s := new([]int) -> len %d (append в nil-срез законен)\n", len(*sn))
sl := &[]int{}
*sl = append(*sl, 1)
fmt.Printf(" s := &[]int{} -> len %d\n", len(*sl))
show("2) СТАРЫЙ МАССИВ ПОСЛЕ ПЕРЕВЫДЕЛЕНИЯ")
s := make([]byte, 8<<20)
e := &s[0]
s = append(s, 1)
fmt.Println(" s := make([]byte, 8 МБ); e := &s[0]; s = append(s, 1)")
*e = 5
fmt.Printf(" *e = 5; *e, s[0] -> %d %d (запись законна, но в старый массив)\n", *e, s[0])
fmt.Printf(" живая куча после GC, e жив -> %d МБ\n", heapMB())
runtime.KeepAlive(e)
e = nil
fmt.Printf(" живая куча после GC, e = nil -> %d МБ\n", heapMB())
runtime.KeepAlive(s)
fmt.Println(" Пока жив e, жив и весь старый массив в 8 МБ.")
show("3) ВЫДЕЛЕНИЯ ЗАВИСЯТ ОТ МЕСТА ВЫЗОВА")
fmt.Println(" newCounterInl встраивается (//go:noinline нет):")
fmt.Printf(" p := newCounterInl(); *p++; sinkN = *p -> %.0f\n",
testing.AllocsPerRun(1000, func() { p := newCounterInl(); *p++; sinkN = *p }))
fmt.Printf(" sinkP = newCounterInl() -> %.0f\n",
testing.AllocsPerRun(1000, func() { sinkP = newCounterInl() }))
fmt.Println(" newCounter с //go:noinline:")
fmt.Printf(" p := newCounter(); *p++; sinkN = *p -> %.0f\n",
testing.AllocsPerRun(1000, func() { p := newCounter(); *p++; sinkN = *p }))
fmt.Println(" Выделений на вызов, testing.AllocsPerRun. Один и тот же код")
fmt.Println(" «return &c» стоит 0 или 1 выделение в зависимости от того, встроен ли")
fmt.Println(" вызов и уходит ли указатель наружу.")
}