Deep Engineering

ЗАМЕР

bench/goptr/practice.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 раза.

Скрипт

167 строк
//go:build ignore

// Две задачи урока про указатели: обе отвечают прогоном.
//
// ЗАДАЧА 1 — ПРЕДСКАЗАТЬ ВЫВОД. Три места, где интуиция «указатель — это
// ссылка» подводит: присваивание параметру-указателю не видно снаружи,
// изменение через него — видно; указатель на элемент среза после append,
// который перевыделил массив, пишет в старый массив.
//
// ЗАДАЧА 2 — ОЦЕНИТЬ КРАТНОСТЬ. Во сколько раз функция, которая создаёт
// структуру в килобайт и возвращает указатель на неё (newBig), дороже той,
// что возвращает ту же структуру значением (makeBig). Указатель на
// переменную функции заставляет её уйти в кучу, а выделение дороже копии.
//
// Рядом — цена «для раздела Глубже»: передача в функцию 16 Б и 1 КБ по
// значению и по указателю. Она объясняет, откуда берётся совет «передавайте
// большое по указателю», и показывает, что он верен только для передачи.
//
// ЗАПУСК:
//
//	go run bench/goptr/practice.go
package main

import (
	"fmt"
	"runtime"
	"testing"
	"unsafe"
)

// ---------------------------------------------------------------- задача 1

func reset(p *int) { p = nil }
func set(p *int)   { *p = 7 }

func task1() {
	x := 1
	p := &x
	reset(p)
	fmt.Println(p == nil)
	set(p)
	fmt.Println(x)
	s := []int{1, 2, 3}
	q := &s[0]
	s = append(s, 4)
	*q = 100
	fmt.Println(s[0], *q)
}

// ---------------------------------------------------------------- задача 2

type Small struct{ A, B int64 } // 16 байт
type Big struct{ A [128]int64 } // 1024 байта

//go:noinline
func smallByValue(s Small) int64 { return s.A + s.B }

//go:noinline
func smallByPointer(s *Small) int64 { return s.A + s.B }

//go:noinline
func bigByValue(b Big) int64 { return b.A[0] + b.A[127] }

//go:noinline
func bigByPointer(b *Big) int64 { return b.A[0] + b.A[127] }

// makeBig возвращает значение: копия, но без кучи.
//
//go:noinline
func makeBig() Big {
	var b Big
	b.A[0] = 1
	return b
}

// newBig возвращает указатель на свою переменную: она уходит в кучу.
//
//go:noinline
func newBig() *Big {
	b := &Big{}
	b.A[0] = 1
	return b
}

var sink int64

type result struct {
	ns     float64
	allocs int64
	bytes  int64
}

// best — лучший из 11 кругов testing.Benchmark: минимум отсекает помехи
// соседей по машине, но не делает число свойством языка.
func best(f func(b *testing.B)) result {
	lo := result{ns: 1e18}
	for round := 0; round < 11; round++ {
		r := testing.Benchmark(func(b *testing.B) {
			b.ReportAllocs()
			f(b)
		})
		if ns := float64(r.T.Nanoseconds()) / float64(r.N); ns < lo.ns {
			lo = result{ns: ns, allocs: r.AllocsPerOp(), bytes: r.AllocedBytesPerOp()}
		}
	}
	return lo
}

func main() {
	fmt.Println("ЗАДАЧА 1 — предсказать вывод")
	fmt.Println()
	task1()

	fmt.Println()
	fmt.Println("ЗАДАЧА 2 — цена: значение против указателя")
	fmt.Println()
	small := Small{1, 2}
	big := Big{}
	big.A[0], big.A[127] = 1, 2

	sv := best(func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			sink = smallByValue(small)
		}
	})
	sp := best(func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			sink = smallByPointer(&small)
		}
	})
	bv := best(func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			sink = bigByValue(big)
		}
	})
	bp := best(func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			sink = bigByPointer(&big)
		}
	})
	fmt.Printf("  размер   значение   указатель   кратность\n")
	fmt.Printf("  %4d Б   %6.2f нс  %6.2f нс   %5.2f\n", unsafe.Sizeof(small), sv.ns, sp.ns, sv.ns/sp.ns)
	fmt.Printf("  %4d Б   %6.2f нс  %6.2f нс   %5.2f\n", unsafe.Sizeof(big), bv.ns, bp.ns, bv.ns/bp.ns)

	fmt.Println()
	fmt.Println("  Оговорка: указатель, возвращённый из функции, — это куча")
	mv := best(func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			v := makeBig()
			sink += v.A[0]
		}
	})
	np := best(func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			p := newBig()
			sink += p.A[0]
		}
	})
	fmt.Printf("  makeBig() Big    %6.2f нс   выделений %d, байт %d\n", mv.ns, mv.allocs, mv.bytes)
	fmt.Printf("  newBig()  *Big   %6.2f нс   выделений %d, байт %d\n", np.ns, np.allocs, np.bytes)
	fmt.Printf("  кратность newBig / makeBig        %.1f\n", np.ns/mv.ns)

	fmt.Println()
	fmt.Printf("  Лучший из 11 кругов. %s %s/%s, GOMAXPROCS=%d\n",
		runtime.Version(), runtime.GOOS, runtime.GOARCH, runtime.GOMAXPROCS(0))
}