Deep Engineering

ЗАМЕР

bench/goptr/internals.go

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/interview/golang/pointers
Как запустить
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

Запись прогона

Замеры для урока «Указатели в 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 раза.

Скрипт

290 строк
//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("  адрес этого указателя — и это опять передача по значению.")
}