MEASUREMENT
bench/goptr/internals.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
290 lines//go:build ignore
// Указатели в Go: что копируется при вызове, что нет — и где указатель
// перестаёт указывать туда, куда кажется.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Вопрос «передаётся ли в Go что-нибудь по ссылке» звучит
// как вопрос на определение, а проверяет понимание: кто это понимает, тот
// предскажет, что будет после p = nil внутри функции, после append, который
// перевыделил массив, и после вызова метода на nil-указателе. Всё это
// проверяется запуском, а не пересказом.
//
// АДРЕСА. Настоящие адреса меняются от запуска к запуску, поэтому печатаются
// не они, а метки «<адрес N>»: одинаковый адрес — одинаковая метка, номер —
// порядок первого появления. Равенство и неравенство адресов при этом
// сохраняются, а только они и важны.
//
// ДЕВЯТЬ СЮЖЕТОВ:
//
// 1. & и *: адрес и разыменование; p.X и вызов метода без звёздочки.
// 2. Параметр-указатель — копия: изменение через него видно снаружи,
// присваивание ему — нет.
// 3. nil-указатель: разыменование — паника; метод на nil-получателе законен.
// 4. new(T) и &T{}: одно и то же.
// 5. Адрес элемента среза и append, который перевыделил массив.
// 6. Указатель на локальную переменную пережил функцию.
// 7. Арифметики указателей нет — только через unsafe.
// 8. Срез и карта в параметре: копируется заголовок, а не данные.
// 9. Указатель на указатель: как функции поменять указатель вызывающего.
//
// Ошибки компиляции (адрес элемента карты, p + 1, &10) — в compile.sh;
// решения escape-анализа — в escape.sh.
//
// ЗАПУСК:
//
// go run bench/goptr/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))))
}
// ------------------------------------------------------------ метки адресов
var labels = map[string]int{}
// addr превращает адрес в метку «<адрес N>»: одинаковые адреса — одинаковые
// метки, номера — по порядку первого появления.
func addr(p any) string {
key := fmt.Sprintf("%p", p)
if key == "0x0" {
return "nil"
}
n, ok := labels[key]
if !ok {
n = len(labels) + 1
labels[key] = n
}
return fmt.Sprintf("<адрес %d>", n)
}
// panicText выполняет f и возвращает текст паники или пустую строку.
func panicText(f func()) (msg string) {
defer func() {
if r := recover(); r != nil {
msg = fmt.Sprint(r)
}
}()
f()
return ""
}
// ---------------------------------------------------------- 1. & и *
type Point struct{ X, Y int }
func (p *Point) Move(dx int) { p.X += dx } // получатель — указатель
func (p Point) Sum() int { return p.X + p.Y } // получатель — значение
// ------------------------------------------------- 2. копия указателя
// set меняет переменную, на которую указывает p: это видно снаружи.
func set(p *int) { *p = 42 }
// redirect перенаправляет СВОЮ копию указателя и пишет уже не в x.
func redirect(p *int) {
y := 0
p = &y
*p = 99
}
// drop обнуляет свою копию указателя.
func drop(p *int) { p = nil }
// where печатает, что лежит в параметре и где лежит он сам.
func where(p *int) (string, string) { return addr(p), addr(&p) }
// ------------------------------------------------------------- 3. nil
type Tree struct {
Val int
Left, Right *Tree
}
// Sum проверяет nil сам — и поэтому работает на пустом дереве.
func (t *Tree) Sum() int {
if t == nil {
return 0
}
return t.Val + t.Left.Sum() + t.Right.Sum()
}
// Kind не трогает полей вовсе.
func (t *Tree) Kind() string { return "tree" }
// ----------------------------------------------- 6. указатель на локальную
//go:noinline
func newCounter() *int {
c := 0
return &c
}
//go:noinline
func localOnly() int {
c := 0
p := &c
*p++
return c
}
var sinkP *Point
var sinkN int
// ------------------------------------------- 8. срез и карта в параметре
func setFirst(s []int) { s[0] = 100 }
func grow(s []int) { s = append(s, 4) }
func put(m map[string]int) { m["k"] = 1 }
func resetMap(m map[string]int) { m = nil }
// --------------------------------------------- 9. указатель на указатель
var other = 7
func retarget(pp **int) { *pp = &other }
func main() {
fmt.Printf("%s %s/%s | указатели\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
show("1) АДРЕС И РАЗЫМЕНОВАНИЕ")
x := 10
p := &x
fmt.Printf(" &x -> %s\n", addr(&x))
fmt.Printf(" p := &x; p -> %s\n", addr(p))
fmt.Printf(" *p -> %d\n", *p)
*p = 20
fmt.Printf(" *p = 20; x -> %d\n", x)
pt := &Point{1, 2}
fmt.Println(" pt := &Point{1, 2}; var v Point")
fmt.Printf(" pt.X -> %d (то же, что (*pt).X)\n", pt.X)
fmt.Printf(" pt.Sum() -> %d (Sum на Point: взято *pt)\n", pt.Sum())
var v Point
v.Move(5)
fmt.Printf(" v.Move(5); v.X -> %d (Move на *Point: взято &v)\n", v.X)
show("2) ПАРАМЕТР-УКАЗАТЕЛЬ — КОПИЯ")
labels = map[string]int{}
x = 1
p = &x
fmt.Printf(" снаружи p = %s &p = %s\n", addr(p), addr(&p))
in, inAddr := where(p)
fmt.Printf(" внутри p = %s &p = %s\n", in, inAddr)
fmt.Println(" Значение то же — адрес x; переменная-указатель другая.")
fmt.Println()
set(p)
fmt.Printf(" set(p): *p = 42 x = %d — видно\n", x)
redirect(p)
fmt.Printf(" redirect(p): p = &y; *p = 99 x = %d, p = %s — не видно\n", x, addr(p))
drop(p)
fmt.Printf(" drop(p): p = nil p == nil: %v — не видно\n", p == nil)
show("3) nil-УКАЗАТЕЛЬ")
var np *Point
fmt.Printf(" var np *Point; np == nil -> %v\n", np == nil)
fmt.Printf(" *np -> паника: %s\n", panicText(func() { _ = *np }))
fmt.Printf(" np.X -> паника: %s\n", panicText(func() { _ = np.X }))
fmt.Printf(" np.Sum() (на Point) -> паника: %s\n", panicText(func() { _ = np.Sum() }))
var nt *Tree
fmt.Printf(" nt.Kind() (на *Tree) -> %q\n", nt.Kind())
fmt.Printf(" nt.Sum() (на *Tree) -> %d\n", nt.Sum())
t := &Tree{Val: 1, Left: &Tree{Val: 2}}
fmt.Printf(" дерево 1 + 2, t.Sum() -> %d\n", t.Sum())
_, isRuntime := func() (r any, ok bool) {
defer func() {
r = recover()
_, ok = r.(runtime.Error)
}()
_ = *np
return
}()
fmt.Printf(" значение паники — runtime.Error: %v\n", isRuntime)
show("4) new(T) И &T{}")
a := new(Point)
b := &Point{}
fmt.Printf(" a := new(Point) -> %+v\n", *a)
fmt.Printf(" b := &Point{} -> %+v\n", *b)
fmt.Printf(" *a == *b -> %v\n", *a == *b)
fmt.Printf(" a == b -> %v (две разные переменные)\n", a == b)
fmt.Printf(" выделений, если указатель уходит в глобальную переменную:\n")
fmt.Printf(" sinkP = new(Point) -> %.0f\n", testing.AllocsPerRun(1000, func() { sinkP = new(Point) }))
fmt.Printf(" sinkP = &Point{} -> %.0f\n", testing.AllocsPerRun(1000, func() { sinkP = &Point{} }))
n := new(int)
fmt.Printf(" new(int); *n -> %d (&int{} не бывает: у int нет составного литерала)\n", *n)
show("5) АДРЕС ЭЛЕМЕНТА СРЕЗА И append")
s := []int{1, 2, 3}
e := &s[0]
*e = 10
fmt.Printf(" s := []int{1, 2, 3}; e := &s[0]; *e = 10 -> s = %v\n", s)
fmt.Printf(" len %d, cap %d\n", len(s), cap(s))
s = append(s, 4)
fmt.Printf(" s = append(s, 4) -> len %d, cap %d\n", len(s), cap(s))
fmt.Printf(" e == &s[0] -> %v (массив перевыделен)\n", e == &s[0])
*e = 100
fmt.Printf(" *e = 100 -> s[0] = %d, *e = %d\n", s[0], *e)
s2 := make([]int, 3, 8)
e2 := &s2[0]
s2 = append(s2, 4)
*e2 = 100
fmt.Printf(" с запасом: make([]int, 3, 8), e2 := &s2[0], append, *e2 = 100\n")
fmt.Printf(" e2 == &s2[0] -> %v\n", e2 == &s2[0])
fmt.Printf(" s2[0] -> %d\n", s2[0])
show("6) УКАЗАТЕЛЬ НА ЛОКАЛЬНУЮ ПЕРЕМЕННУЮ")
labels = map[string]int{}
c1 := newCounter()
c2 := newCounter()
*c1 += 1
fmt.Printf(" c1, c2 := newCounter(), newCounter(); *c1 += 1\n")
fmt.Printf(" *c1, *c2 -> %d %d\n", *c1, *c2)
fmt.Printf(" c1 == c2 -> %v (каждый вызов — своя переменная)\n", c1 == c2)
fmt.Printf(" выделений на вызов:\n")
fmt.Printf(" newCounter() (return &c) -> %.0f\n", testing.AllocsPerRun(1000, func() { _ = newCounter() }))
fmt.Printf(" localOnly() (&c не уходит) -> %.0f\n", testing.AllocsPerRun(1000, func() { sinkN = localOnly() }))
show("7) АРИФМЕТИКА — ТОЛЬКО ЧЕРЕЗ unsafe")
arr := [4]int32{10, 20, 30, 40}
q := &arr[0]
fmt.Printf(" arr := [4]int32{10, 20, 30, 40}; q := &arr[0]\n")
fmt.Printf(" unsafe.Sizeof(arr[0]) -> %d\n", unsafe.Sizeof(arr[0]))
fmt.Printf(" *(*int32)(unsafe.Add(unsafe.Pointer(q), 4)) -> %d\n", *(*int32)(unsafe.Add(unsafe.Pointer(q), 4)))
fmt.Printf(" *(*int32)(unsafe.Add(unsafe.Pointer(q), 12)) -> %d\n", *(*int32)(unsafe.Add(unsafe.Pointer(q), 12)))
fmt.Printf(" без unsafe: arr[1] -> %d\n", arr[1])
fmt.Println(" q + 1 и q++ не собираются (compile.sh).")
show("8) СРЕЗ И КАРТА В ПАРАМЕТРЕ: КОПИЯ ЗАГОЛОВКА")
sl := []int{1, 2, 3}
setFirst(sl)
fmt.Printf(" setFirst(s): s[0] = 100 снаружи s = %v — видно\n", sl)
grow(sl)
fmt.Printf(" grow(s): s = append(s, 4) снаружи len(s) = %d — не видно\n", len(sl))
m := map[string]int{}
put(m)
fmt.Printf(" put(m): m[\"k\"] = 1 снаружи len(m) = %d — видно\n", len(m))
resetMap(m)
fmt.Printf(" resetMap(m): m = nil снаружи m == nil: %v — не видно\n", m == nil)
show("9) УКАЗАТЕЛЬ НА УКАЗАТЕЛЬ")
labels = map[string]int{}
x = 1
p = &x
fmt.Printf(" до: p = %s (адрес x), *p = %d\n", addr(p), *p)
retarget(&p)
fmt.Printf(" retarget(&p): *pp = &other\n")
fmt.Printf(" после: p = %s (адрес other), *p = %d\n", addr(p), *p)
fmt.Println(" Чтобы функция поменяла указатель вызывающего, ей передают")
fmt.Println(" адрес этого указателя — и это опять передача по значению.")
}