Указатели в Go: всё передаётся по значению, даже указатель
Собеседование по указателям идёт лестницей: что такое указатель — передаётся ли в Go что-нибудь по ссылке — что будет после p = nil внутри функции — можно ли вызвать метод на nil — почему у элемента карты нет адреса — можно ли вернуть указатель на локальную переменную — есть ли арифметика указателей. Прогон отвечает на каждый шаг: присваивание параметру-указателю снаружи не видно, метод на nil-получателе отработал, указатель на элемент среза после append пишет в старый массив, а функция, вернувшая указатель на свою переменную, в замере на go1.24.7 обошлась дороже копии в килобайт.
Полное техническое изложение
TL;DR
Указатель — это значение, в котором лежит адрес переменной. &x берёт адрес,
*p даёт переменную по адресу, нулевое значение указателя — nil. Обращаться к
полю и вызывать метод можно без звёздочки: p.X — это (*p).X, а v.Move(5) у
метода на *Point — это (&v).Move(5). new(T) и &T{} дают указатель на
новую переменную; для структуры это одно и то же, а для среза и карты — нет:
new даёт указатель на nil-срез или nil-карту, &T{} — на пустой, но
готовый к записи контейнер.
В Go всё передаётся по значению, и указатель тоже: функция получает копию
адреса. Прогон показывает это меткой адреса: снаружи p = <адрес 1>,
&p = <адрес 2>, внутри функции p = <адрес 1>, &p = <адрес 3> — значение
то же, переменная другая. Поэтому *p = 42 внутри видно снаружи, а p = &y и
p = nil — нет. Общая у вызывающего и функции не переменная-указатель, а
переменная, на которую он указывает.
Дальше — то, что отличает понимание от пересказа. Разыменование nil —
паника runtime error: invalid memory address or nil pointer dereference, но
метод на nil-получателе законен и отработал, пока не тронул поля. Адрес
элемента карты взять нельзя — ошибка компиляции, — а элемента среза можно, и
после append, который перевыделил массив, такой указатель теряет связь с
текущим массивом среза: запись через него законна, но уходит в старый массив —
s[0] = 10, *e = 100, — и старый массив живёт, пока жив указатель. Указатель на
локальную переменную вернуть законно: переменная живёт, пока на неё есть
указатель, а где её разместить, решает компилятор — для отдельно
скомпилированной функции он пишет moved to heap: c (переносит в кучу —
память, которая переживает вызов). Арифметики указателей нет: p + 1 не
собирается, обходной путь только через unsafe. А цена — наблюдение одной
конфигурации, go1.24.7 linux/amd64: передать килобайт по указателю оказалось
дешевле, чем по значению, но функция, вернувшая указатель на свою новую
переменную, — дороже той, что вернула ту же структуру значением, из-за
выделения в куче. Числа — в «Глубже»; на своей машине их надо мерить.
- как объявить структуру, её поля и нулевое значение — урок о структурах;
- как пользоваться срезом и картой:
append,len,cap, запись по ключу — подробно в уроках о срезах и картах (они дальше по линии); - что метод объявляется с получателем — подробно в уроке о получателе метода.
- что такое адрес,
&и*, и почемуp.Xпишется без звёздочки; - что такое адресуемость, escape-анализ и флаг
-gcflags='-m'— нужное объясняется по ходу, подробное — в своих уроках; - что такое
unsafe.Pointer.
Что здесь на самом деле спрашивают
Лестница обычно такая:
- «Что такое указатель и зачем он нужен?» — разминка; ответ «адрес переменной» верный, но ничего не объясняет.
- «Передаётся ли в Go что-нибудь по ссылке?» — главный вопрос урока. Ответ «указатели и карты передаются по ссылке» — та самая ошибка, которую здесь и ловят.
- «Что будет, если внутри функции присвоить параметру-указателю
nil?» — проверка того же самого на коде. - «Можно ли вызвать метод на nil-указателе?» — и что при этом упадёт, а что нет.
- «Почему нельзя взять адрес элемента карты, а элемента среза — можно?» — про адресуемость и про указатель, который тихо потерял связь с массивом среза.
- «Можно ли вернуть указатель на локальную переменную?» — вопрос из C; в Go ответ другой.
- «Есть ли в Go арифметика указателей?» — и что делать, если очень нужно.
- «Что быстрее — передать структуру по значению или по указателю?» — про цену, и у неё две стороны.
Стержень у лестницы один: указатель — обычное значение, которое копируется, как любое другое; общей у копий остаётся только переменная, на которую они указывают. Всё, что в этом уроке выглядит неожиданным, из этой фразы и следует.
База: адрес переменной и переменная по адресу
Спецификация определяет указатель через переменную, на которую он указывает:
A pointer type denotes the set of all pointers to variables of a given type,
called the base type of the pointer. The value of an uninitialized pointer is
nil.
Тип-указатель обозначает множество всех указателей на переменные данного типа, называемого базовым типом указателя. Значение неинициализированного указателя — nil.
Два оператора: &x даёт адрес переменной x, *p — переменную, на которую
указывает p. Через *p можно и читать, и писать. В прогоне x := 10 и тип с
двумя методами.
Фрагмент bench/goptr/internals.go (объявления уровня пакета):
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 } // получатель — значениеВывод bench/goptr/internals.go, раздел 1:
1) АДРЕС И РАЗЫМЕНОВАНИЕ
────────────────────────
&x -> <адрес 1>
p := &x; p -> <адрес 1>
*p -> 10
*p = 20; x -> 20
pt := &Point{1, 2}; var v Point
pt.X -> 1 (то же, что (*pt).X)
pt.Sum() -> 3 (Sum на Point: взято *pt)
v.Move(5); v.X -> 5 (Move на *Point: взято &v)
Настоящие адреса меняются от запуска к запуску, поэтому скрипт печатает метки:
одинаковый адрес — одинаковая метка <адрес N>. p и &x — одна метка, то
есть одно значение.
Последние три строки — то, из-за чего в Go почти не видно звёздочек.
Поле через указатель читается без разыменования: правило селектора в
спецификации работает for a value x of type T or *T
(для значения x типа T или *T),
поэтому pt.X — это (*pt).X. Метод вызывается так же:
pt.Sum() у метода на Point компилятор превращает в (*pt).Sum(), а
v.Move(5) у метода на *Point — в (&v).Move(5), потому что v адресуема
(то есть у неё можно взять адрес: это переменная; что ещё адресуемо — Механизм 3).
Набор методов в цитате ниже — это методы, которые можно вызвать у значения
данного типа:
If x is addressable and &x's method set contains m, x.m() is shorthand for
(&x).m()
Если x адресуема и набор методов &x содержит m, то x.m() — сокращение для (&x).m().
Когда этого сокращения нет — например, у значения в интерфейсе или у элемента карты, — разобрано в уроке о получателе метода.
new(T) и &T{}. Оба дают указатель на новую переменную:
Calling the built-in function new or taking the address of a composite literal
allocates storage for a variable at run time.
Вызов встроенной функции new или взятие адреса составного литерала выделяет память под переменную во время выполнения.
Вывод bench/goptr/internals.go, раздел 4:
4) new(T) И &T{}
────────────────
a := new(Point) -> {X:0 Y:0}
b := &Point{} -> {X:0 Y:0}
*a == *b -> true
a == b -> false (две разные переменные)
выделений, если указатель уходит в глобальную переменную:
sinkP = new(Point) -> 1
sinkP = &Point{} -> 1
new(int); *n -> 0 (&int{} не бывает: у int нет составного литерала)
Для структуры Point обе формы дают указатели на равные нулевые значения, а
сами указатели различны: каждое выражение создаёт свою переменную. Выделений в
этом контексте по одному, и решения компилятора о месте (Механизм 4) у обоих
одинаковые; число выделений — наблюдение реализации go1.24.7, а не правило
языка. &T{...} умеет сразу заполнить поля, а new работает с любым типом, в
том числе с int, у которого составного литерала нет.
Для среза и карты формы расходятся. new(T) даёт указатель на нулевое
значение типа, а у среза и карты нулевое значение — nil. &T{} даёт
указатель на пустой, но инициализированный контейнер. Спецификация говорит об
этом прямо:
Note that the zero value for a slice or map type is not the same as an
initialized but empty value of the same type.
Заметьте, что нулевое значение типа среза или карты — не то же самое, что инициализированное, но пустое значение того же типа.
Вывод bench/goptr/audit.go, раздел 1:
1) new(T) И &T{} ДЛЯ СТРУКТУРЫ, СРЕЗА И КАРТЫ
─────────────────────────────────────────────
T *new(T) *(&T{})
Point {X:0 Y:0} {X:0 Y:0}
[]int == nil: true == nil: false
map[string]int == nil: true == nil: false
запись (*m)["k"] = 1:
m := new(map[string]int) -> паника: assignment to entry in nil map
m := &map[string]int{} -> len 1
*s = append(*s, 1):
s := new([]int) -> len 1 (append в nil-срез законен)
s := &[]int{} -> len 1
Для среза разница почти не видна: append в nil-срез законен. Для карты она
решает: в карту из new писать нельзя — паника, — её сначала создают через
make или литерал. Поэтому указатель на карту почти всегда пишут как
&map[K]V{} или вовсе не нужен: карта сама по себе — ссылка на общую таблицу
(об этом — в уроке о картах).
Адреса нет ни у константы, ни у результата вызова функции.
Вывод bench/goptr/compile.sh, случай addrconst (файл намеренно не собирается):
addrconst:
./main.go:10:8: invalid operation: cannot take address of 10 (untyped int constant)
./main.go:11:8: invalid operation: cannot take address of get() (value of struct type Point)
Этого достаточно для ответа уровня Junior на вопрос «что такое указатель»:
значение, в котором лежит адрес переменной. & берёт адрес, * даёт
переменную по адресу, нулевое значение — nil. Через указатель функция может
изменить переменную вызывающего, не копируя её. Последняя фраза — ровно та,
которую на следующем шаге и проверяют.
Механизм 1: параметр-указатель — копия
Что происходит при вызове функции, спецификация говорит прямо:
After they are evaluated, new storage is allocated for the function's
variables, which includes its parameters and results. Then, the arguments of the
call are passed to the function, which means that they are assigned to their
corresponding function parameters, and the called function begins execution.
После вычисления выделяется память под переменные функции, включая её параметры и результаты. Затем аргументы вызова передаются функции — то есть присваиваются соответствующим параметрам, — и вызванная функция начинает выполняться.
Аргументы присваиваются параметрам — исключений для указателей в этом
правиле нет. Параметр p *int — новая переменная функции, в которую
скопирован адрес. Три функции с одинаковой сигнатурой.
Фрагмент bench/goptr/internals.go:
func set(p *int) { *p = 42 }
func redirect(p *int) {
y := 0
p = &y
*p = 99
}
func drop(p *int) { p = nil }
func where(p *int) (string, string) { return addr(p), addr(&p) } // печатает значение p и адрес самого pВывод bench/goptr/internals.go, раздел 2:
2) ПАРАМЕТР-УКАЗАТЕЛЬ — КОПИЯ
─────────────────────────────
снаружи p = <адрес 1> &p = <адрес 2>
внутри p = <адрес 1> &p = <адрес 3>
Значение то же — адрес x; переменная-указатель другая.
set(p): *p = 42 x = 42 — видно
redirect(p): p = &y; *p = 99 x = 42, p = <адрес 1> — не видно
drop(p): p = nil p == nil: false — не видно
Строку «внутри» печатает where — такая же функция с параметром p *int.
Первые две строки — весь механизм. Внутри функции p хранит тот же
<адрес 1>, но сама переменная p лежит по другому адресу: <адрес 3>
против <адрес 2>. Это две переменные с одинаковым значением. Дальше всё
следует само:
*p = 42пишет по адресу — вx, общую для обеих копий. Снаружи это видно.p = &yменяет саму копию. Внешнийpоб этом не знает и по-прежнему указывает наx; 99 записано вy.p = nil— то же самое: обнулилась копия.
Поэтому ответ на вопрос «передаётся ли что-нибудь по ссылке» — нет. Всё
передаётся по значению; у указателя значение — адрес, и копия адреса ведёт к
той же переменной. Слово «по ссылке» в C++ означает, что параметр — другое
имя той же переменной, и присваивание ему меняет переменную вызывающего. В Go
такого параметра нет; проверка — drop(p).
Всё это помещается в короткую программу. Полный файл
bench/goptr/cases/example/main.go — его можно скопировать в пустой каталог
рядом с go.mod (module example, go 1.24) и запустить командой go run .:
package main
import "fmt"
func set(p *int) { *p = 42 }
func drop(p *int) { p = nil }
func main() {
x := 1
p := &x
set(p)
fmt.Println(x)
drop(p)
fmt.Println(p == nil, *p)
}Вывод bench/goptr/example.sh:
go1.24.7 linux/amd64 | полный пример: go run .
42
false 42
Если функции всё-таки нужно поменять указатель вызывающего, ей передают
адрес этого указателя — и это опять передача по значению, только значение
теперь **int.
Фрагмент bench/goptr/internals.go:
var other = 7
func retarget(pp **int) { *pp = &other }Вывод bench/goptr/internals.go, раздел 9:
9) УКАЗАТЕЛЬ НА УКАЗАТЕЛЬ
─────────────────────────
до: p = <адрес 1> (адрес x), *p = 1
retarget(&p): *pp = &other
после: p = <адрес 2> (адрес other), *p = 7
На практике проще вернуть новый указатель результатом.
Механизм 2: nil — паника при разыменовании, но не при вызове метода
Правило разыменования в спецификации короткое:
If x is nil, an attempt to evaluate *x will cause a run-time panic.
Если x равен nil, попытка вычислить *x вызывает панику во время выполнения.
Ключевое слово — вычислить *x. Не «использовать указатель», а прочитать
или записать переменную по нему.
Фрагмент bench/goptr/internals.go; комментарии сокращены:
type Tree struct {
Val int
Left, Right *Tree
}
func (t *Tree) Sum() int { // проверяет nil сам
if t == nil {
return 0
}
return t.Val + t.Left.Sum() + t.Right.Sum()
}
func (t *Tree) Kind() string { return "tree" } // полей не трогаетВывод bench/goptr/internals.go, раздел 3:
3) nil-УКАЗАТЕЛЬ
────────────────
var np *Point; np == nil -> true
*np -> паника: runtime error: invalid memory address or nil pointer dereference
np.X -> паника: runtime error: invalid memory address or nil pointer dereference
np.Sum() (на Point) -> паника: runtime error: invalid memory address or nil pointer dereference
nt.Kind() (на *Tree) -> "tree"
nt.Sum() (на *Tree) -> 0
дерево 1 + 2, t.Sum() -> 3
значение паники — runtime.Error: true
Три паники и два успешных вызова, и граница между ними проходит ровно по разыменованию:
*npиnp.Xразыменовывают — паника. Про поле спецификация говорит отдельно:assigning to or evaluating x.f causes a run-time panic
(присваивание x.f или его вычисление вызывает панику).np.Sum()— метод на значенииPoint. Чтобы его вызвать, нужно скопировать*npв получатель, а это разыменование — паника.nt.Kind()иnt.Sum()— методы на указателе*Tree. Получатель — просто параметр со значениемnil; паники нет, пока тело не тронет поле.Kindполей не трогает,Sumсначала проверяетt == nil.
Это распространённый приём: рекурсивный Sum не проверяет детей перед
вызовом — пустое поддерево само даёт 0.
Текст паники — из рантайма. В runtime/panic.go (строка 258 на go1.24.7)
лежит memoryError = error(errorString("invalid memory address or nil pointer dereference")),
а префикс runtime error: добавляет errorString.Error в
runtime/error.go. Значение, пойманное recover, — runtime.Error, как и
напечатал прогон. Дело не в тексте: у errorString есть метод
RuntimeError() (там же, строка 79 на go1.24.7), а a type is a run time error if it has a RuntimeError method
(тип считается ошибкой времени выполнения, если у него есть метод RuntimeError).
Одно предупреждение, которое на собеседовании стоит произнести самому:
nil-указатель, положенный в интерфейс, делает интерфейс не-nil. Метод на
nil-получателе вызовется, а err == nil даст false. Это разобрано в уроке
об интерфейсах.
Где go1.24 не паниковал
Гарантию «разыменование nil — паника» go1.24.7 выполняет не всегда:
компилятор мог перенести проверку на nil в ту ветку, где прочитанное значение
используется, и на другом пути она не выполнялась вовсе. В go1.25.1 проходу
tighten, который переносит значения ближе к месту использования, запрещено
трогать проверки на nil (src/cmd/compile/internal/ssa/tighten.go, строки
48–49: Nil checks need to stay in their block
(проверки на nil должны оставаться в своём блоке)),
а в дистрибутиве есть регрессионный тест test/fixedbugs/issue72860.go.
Функция из него и привычный по виду код, который читает результат до
проверки ошибки.
Фрагмент bench/goptr/nilcheck.go; комментарии убраны:
//go:noinline
func f(p *int, b bool) int {
valid := *p >= 0
if !b || !valid {
return 5
}
return 6
}
func open() string {
file, err := os.Open("no-such-file")
name := file.Name()
if err != nil {
return "ошибка открытия, имя не используется"
}
return name
}Вывод bench/goptr/nilcheck.go на go1.24.7:
go1.24.7 linux/amd64 | проверка на nil, которую компилятор откладывал
f(nil, false) -> 5
open() -> ошибка открытия, имя не используется
Вывод того же файла на go1.25.1:
go1.25.1 linux/amd64 | проверка на nil, которую компилятор откладывал
f(nil, false) -> паника: runtime error: invalid memory address or nil pointer dereference
open() -> паника: runtime error: invalid memory address or nil pointer dereference
file.Name() — это return f.name (os/file.go, строка 62 на go1.24.7 и 63
на go1.25.1): чтение поля
через nil. На go1.24.7 код «работал», на go1.25.1 он падает — как и обязан
по спецификации. Практический вывод: код, который трогает результат до
проверки ошибки, мог проходить тесты и начать падать после обновления
компилятора. Исправлять надо код, а не версию: сначала
if err != nil, потом результат.
Механизм 3: адрес есть у переменной — у элемента среза есть, у элемента карты нет
& применим не ко всему. Спецификация перечисляет, что адресуемо:
The operand must be addressable, that is, either a variable, pointer
indirection, or slice indexing operation; or a field selector of an addressable
struct operand; or an array indexing operation of an addressable array.
Операнд должен быть адресуемым, то есть быть переменной, разыменованием указателя или индексированием среза; либо селектором поля адресуемой структуры; либо индексированием адресуемого массива.
Индексирование среза в списке есть, индексирования карты — нет, и компилятор это подтверждает.
Вывод bench/goptr/compile.sh, случаи mapaddr и mapfield (файлы намеренно не собираются):
mapaddr:
./main.go:9:8: invalid operation: cannot take address of m["a"] (map index expression of struct type Point)
mapfield:
./main.go:9:2: cannot assign to struct field m["a"].X in map
Вторая ошибка — следствие первой. m["a"].X = 10 —
присваивание полю структуры, лежащей в карте; поле адресуемо только у
адресуемой структуры, а элемент карты не адресуем. Лечится так: достать
значение, изменить и положить обратно — либо хранить в карте указатели.
Почему у карты так устроено — записи при росте переезжают в новую таблицу, —
разобрано в уроке о картах.
У среза адрес элемента взять можно — и тут же видно, что бывает с адресом элемента, который переехал.
Вывод bench/goptr/internals.go, раздел 5:
5) АДРЕС ЭЛЕМЕНТА СРЕЗА И append
────────────────────────────────
s := []int{1, 2, 3}; e := &s[0]; *e = 10 -> s = [10 2 3]
len 3, cap 3
s = append(s, 4) -> len 4, cap 6
e == &s[0] -> false (массив перевыделен)
*e = 100 -> s[0] = 10, *e = 100
с запасом: make([]int, 3, 8), e2 := &s2[0], append, *e2 = 100
e2 == &s2[0] -> true
s2[0] -> 100
Запаса не было, append выделил новый массив и скопировал туда элементы.
s теперь смотрит в новый массив, а e — в старый. Запись *e = 100
прошла без ошибки, но в массив, который срез больше не видит: s[0] = 10.
С запасом ёмкости массив тот же, и связь сохраняется.
Важно, чего здесь нет: висячего указателя, как в C. Указатель e не
испорчен — читать и писать через него законно, и старый массив никуда не
делся. Именно поэтому он и остаётся в памяти, пока жив e.
Вывод bench/goptr/audit.go, раздел 2:
2) СТАРЫЙ МАССИВ ПОСЛЕ ПЕРЕВЫДЕЛЕНИЯ
────────────────────────────────────
s := make([]byte, 8 МБ); e := &s[0]; s = append(s, 1)
*e = 5; *e, s[0] -> 5 0 (запись законна, но в старый массив)
живая куча после GC, e жив -> 18 МБ
живая куча после GC, e = nil -> 10 МБ
Пока жив e, жив и весь старый массив в 8 МБ.
Отсюда правило для кода: указатель на элемент среза связан с текущим
массивом среза, пока append не перевыделил массив; после этого он работает
со старым массивом и удерживает его в памяти. Держать такой указатель через
append — значит зависеть от ёмкости, которую обычно никто не контролирует.
Как устроен рост и когда массив перевыделяется — в уроке о
срезах.
Механизм 4: указатель на локальную переменную — законно
В C вернуть адрес локальной переменной — ошибка: кадр функции освобождён, и
указатель смотрит в мусор. В Go это обычный конструктор: переменная живёт,
пока на неё есть указатель. Где её разместить — в кадре функции (на стеке) или
в куче, памяти, которая переживает вызов, — решает компилятор. В прогоне
функция помечена //go:noinline, то есть не встраивается в место вызова.
Фрагмент bench/goptr/internals.go:
//go:noinline
func newCounter() *int {
c := 0
return &c
}Вывод bench/goptr/internals.go, раздел 6:
6) УКАЗАТЕЛЬ НА ЛОКАЛЬНУЮ ПЕРЕМЕННУЮ
────────────────────────────────────
c1, c2 := newCounter(), newCounter(); *c1 += 1
*c1, *c2 -> 1 0
c1 == c2 -> false (каждый вызов — своя переменная)
выделений на вызов:
newCounter() (return &c) -> 1
localOnly() (&c не уходит) -> 0
Каждый вызов даёт свою переменную, и она пережила функцию. В этом контексте — функция не встраивается, результат нужен вызывающему — каждый вызов стоил одного выделения памяти. Спецификация о стеке и куче не говорит ничего — она говорит о переменных. Решение компилятора можно увидеть.
Вывод bench/goptr/escape.sh (файл cases/escape/main.go):
go1.24.7 linux/amd64 | go build -gcflags='-m -l': решения компилятора
./main.go:9:2: moved to heap: c
./main.go:22:34: new(Point) escapes to heap
./main.go:24:31: &Point{} escapes to heap
./main.go:28:10: new(Point) does not escape
./main.go:34:7: &Point{} does not escape
Строка 9 — c в newCounter: адрес возвращается, переменная обязана пережить
кадр, и компилятор переносит её в кучу. Строки 22–34 — new(Point) и
&Point{} в одинаковых положениях: когда указатель возвращается, оба
escapes to heap; когда остаётся внутри функции, оба does not escape. Это и
есть ответ на «одно ли и то же new(Point) и &Point{}»: компилятор не
различает их даже здесь.
Из этих строк следует и обратное: взять адрес — ещё не значит выделить
память в куче. У localOnly адрес c взят, но наружу не ушёл, и выделений
ноль.
И ещё одно: moved to heap: c в строке 9 — решение о функции, собранной
отдельно (флаг -l запрещает встраивание). Если функцию можно встроить,
компилятор решает заново в каждом месте вызова. Та же newCounter без
//go:noinline и без -l.
Вывод bench/goptr/inline.sh (файл cases/inline/main.go; local — строка 16, global — строка 23):
go1.24.7 linux/amd64 | go build -gcflags='-m': встраивание и место вызова
./main.go:16:17: inlining call to newCounter
./main.go:23:20: inlining call to newCounter
./main.go:27:7: inlining call to newCounter
./main.go:28:8: inlining call to newCounter
./main.go:7:2: moved to heap: c
./main.go:23:20: moved to heap: c
./main.go:28:8: moved to heap: c
Вывод bench/goptr/audit.go, раздел 3 (выделений на вызов, testing.AllocsPerRun):
3) ВЫДЕЛЕНИЯ ЗАВИСЯТ ОТ МЕСТА ВЫЗОВА
────────────────────────────────────
newCounterInl встраивается (//go:noinline нет):
p := newCounterInl(); *p++; sinkN = *p -> 0
sinkP = newCounterInl() -> 1
newCounter с //go:noinline:
p := newCounter(); *p++; sinkN = *p -> 1
В функции local (строка 16) указатель не покидает её, и строки
moved to heap для этого места нет; замер даёт ноль выделений. В global
(строка 23) указатель уходит в глобальную переменную — одно выделение. Отсюда
правильная формулировка: возвращать адрес локальной переменной корректно;
будет ли выделение и сколько — решает компилятор в конкретном месте вызова.
Числа выделений здесь — наблюдение go1.24.7 (на go1.25.1 они те же), а не
обещание языка. Как компилятор принимает решение и какие ещё случаи уводят
переменную в кучу — урок об escape-анализе.
Механизм 5: арифметики указателей нет — только через unsafe
В C p + 1 — указатель на следующий элемент. В Go это не собирается.
Вывод bench/goptr/compile.sh, случай ptrarith (файл намеренно не собирается):
ptrarith:
./main.go:8:6: invalid operation: p + 1 (mismatched types *int and untyped int)
./main.go:9:2: invalid operation: p++ (non-numeric type *int)
Арифметических операций с указателями нет: ни сложения, ни вычитания, ни
++, ни < между указателями. Сравнивать их можно только на равенство (в
том числе с nil). Поэтому указатель в
Go не выходит за пределы переменной, на которую указывает, — к соседнему
элементу массива переходят индексом, и индекс проверяется на границы.
Если адресная арифметика действительно нужна — например, при разборе
двоичного формата без копирования, — она есть в пакете unsafe.
Вывод bench/goptr/internals.go, раздел 7:
7) АРИФМЕТИКА — ТОЛЬКО ЧЕРЕЗ unsafe
───────────────────────────────────
arr := [4]int32{10, 20, 30, 40}; q := &arr[0]
unsafe.Sizeof(arr[0]) -> 4
*(*int32)(unsafe.Add(unsafe.Pointer(q), 4)) -> 20
*(*int32)(unsafe.Add(unsafe.Pointer(q), 12)) -> 40
без unsafe: arr[1] -> 20
Сдвиг считается в байтах, а не в элементах, и проверки границ нет. Цену документация пакета называет прямо:
Pointer therefore allows a program to defeat the type system and read and write
arbitrary memory. It should be used with extreme care.
Поэтому Pointer позволяет программе обойти систему типов и читать и писать произвольную память. Пользоваться им нужно с крайней осторожностью.
И ещё одно: пакеты, которые импортируют unsafe, по словам той же
документации, may be non-portable and are not protected by the Go 1 compatibility guidelines
(могут быть непереносимыми и не защищены правилами совместимости Go 1).
На собеседовании этого достаточно: арифметики в языке нет, unsafe — выход
для редких мест, и за него платят гарантиями.
Глубже: значение или указатель — цена копии против цены кучи
«Передавайте большие структуры по указателю, это быстрее» — совет, который верен наполовину. Первая половина — копия.
Фрагмент вывода bench/goptr/practice.go, задача 2:
размер значение указатель кратность
16 Б 1.66 нс 1.56 нс 1.07
1024 Б 23.01 нс 1.65 нс 13.92
Функция читает два поля и ничего больше. На 16 байтах разница — доли наносекунды: копировать почти нечего. На килобайте значение дороже в 13,92 раза: каждый вызов копирует 1024 байта. Та же зависимость для получателя метода разобрана в уроке о получателе.
Вторая половина — откуда указатель берётся. Две функции создают одну и ту же структуру в килобайт; одна возвращает её значением, другая — указателем на свою переменную.
Фрагмент bench/goptr/practice.go; комментарии убраны:
//go:noinline
func makeBig() Big {
var b Big
b.A[0] = 1
return b
}
//go:noinline
func newBig() *Big {
b := &Big{}
b.A[0] = 1
return b
}Фрагмент вывода того же прогона:
makeBig() Big 25.62 нс выделений 0, байт 0
newBig() *Big 301.79 нс выделений 1, байт 1024
кратность newBig / makeBig 11.8
Здесь указатель дороже в 11,8 раза. Копия килобайта — несколько десятков
наносекунд; у newBig на каждый вызов приходится выделение 1024 байт в куче,
а потом эту память ещё должен вернуть сборщик мусора. Механизм 4 показал,
откуда выделение: переменная, чей адрес вернули, обязана пережить кадр.
Сводка по этому замеру — вместо правила «указатель быстрее». Механизм объясняет, откуда разница; есть ли она в вашем коде и насколько велика — показывает только замер у вас:
| ситуация | что оказалось дешевле здесь | почему |
|---|---|---|
| передать маленькую структуру | без разницы | копировать почти нечего |
| передать большую структуру, она уже есть | указатель | копируется адрес, а не килобайт |
| создать структуру и вернуть | значение, если указатель уводит её в кучу | выделение здесь дороже копии |
| функция должна изменить переменную вызывающего | указатель | это вопрос смысла, а не цены |
Последняя строка — главная. Выбор между значением и указателем начинают с
вопроса «должна ли функция менять оригинал» и «может ли значения не быть
(nil)», а цену проверяют профилем там, где она заметна, — числа выше
отвечают на вопрос «бывает ли по-разному», а не «как будет в вашем коде».
Как отвечать на собеседовании
Короткий ответ: указатель — значение, в котором лежит адрес переменной. В
Go всё передаётся по значению, указатель тоже: функция получает копию адреса.
Изменение через указатель (*p = 42) видно вызывающему, потому что обе копии
ведут к одной переменной; присваивание самому параметру (p = nil) — нет,
потому что меняется копия.
Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.
Если интервьюер копает глубже
На «по ссылке» покажите две метки адреса. «Снаружи p хранит
<адрес 1> и лежит по <адрес 2>; внутри функции хранит тот же
<адрес 1>, но лежит по <адрес 3>. Значение одно, переменных две — поэтому
p = nil внутри снаружи не виден».
На «метод на nil» — граница по разыменованию. «Метод с получателем-указателем
на nil вызывается: получатель — просто параметр. Паника будет, если тело
тронет поле, или если метод объявлен на значении — тогда нужно скопировать
*p. На этом строят nil-безопасные методы вроде Sum у пустого дерева».
На «адрес элемента карты» — адресуемость. «&m[k] не собирается, и
m[k].X = 1 тоже. У элемента среза адрес есть, но после append, который
перевыделил массив, указатель пишет в старый массив — в прогоне
s[0] = 10, *e = 100».
На «вернуть указатель на локальную» — время жизни отдельно от размещения.
«Законно: переменная живёт, пока на неё есть указатель. Где её разместить,
решает компилятор: для отдельно собранной функции -gcflags=-m печатает
moved to heap: c, но встроенный вызов, чей указатель не ушёл наружу, в
нашем прогоне обошёлся без выделения».
На «что быстрее» — две стороны. «Передать готовую большую структуру по указателю дешевле, чем копировать её. Но вернуть указатель на новую структуру может оказаться дороже, чем вернуть её значением, — из-за выделения в куче; в замере, который я видел, так и было. Насколько — зависит от машины и кода, это меряют».
Дальше спросят
А срез и карта разве не передаются по ссылке?
Нет — тоже по значению. Копируется заголовок среза (указатель на массив, длина, ёмкость) или указатель на внутреннюю структуру карты. Поэтому видно то, что идёт через общий указатель, и не видно то, что меняет саму копию.
Вывод bench/goptr/internals.go, раздел 8:
8) СРЕЗ И КАРТА В ПАРАМЕТРЕ: КОПИЯ ЗАГОЛОВКА
────────────────────────────────────────────
setFirst(s): s[0] = 100 снаружи s = [100 2 3] — видно
grow(s): s = append(s, 4) снаружи len(s) = 3 — не видно
put(m): m["k"] = 1 снаружи len(m) = 1 — видно
resetMap(m): m = nil снаружи m == nil: false — не видно
Это тот же механизм, что у *int: s[0] = 100 пишет через указатель на
массив, а append и m = nil меняют копию. Подробнее — в уроках о
срезах и картах.
Как функции поменять указатель вызывающего?
Два способа. Идиоматичный — вернуть новый указатель: p = f(p). Если
нельзя — передать адрес указателя, **T, как retarget(&p) в Механизме 1:
прогон показал, что после вызова p указывает на other, а *p = 7.
Второй способ встречается реже; если он появился в обычном коде, стоит проверить, нельзя ли просто вернуть новый указатель.
Когда два указателя равны?
Когда указывают на одну переменную или оба nil. В прогоне a := new(Point)
и b := &Point{} дали a == b — false: это две разные переменные с
одинаковым содержимым.
Сравнивать содержимое — *a == *b.
У этого правила есть оговорка в спецификации: Pointers to distinct zero-size variables may or may not be equal
(указатели на разные переменные нулевого размера могут быть равны, а могут и не быть).
Указатели на struct без полей поэтому нельзя использовать как уникальные
метки — равенство двух разных таких переменных не гарантировано ни в ту, ни в
другую сторону.
Почему код, читавший результат до проверки ошибки, начал падать после обновления?
Потому что раньше он должен был падать, но не падал. В go1.24.7
компилятор мог перенести проверку на nil в ветку, где прочитанное значение
используется, и на другом пути она не выполнялась:
f(nil, false) из регрессионного теста issue72860.go вернул 5, а
file.Name() на nil прошёл молча. На go1.25.1 оба случая — паника, как и
требует спецификация.
Чинится не откатом компилятора, а порядком строк: сначала if err != nil,
потом результат. Код, который полагался на старое поведение, был ошибочным и
раньше — просто это не было видно.
Частые заблуждения
Указатели в Go передаются по ссылке
Всё передаётся по значению; у указателя значение — адрес. В прогоне внутри функции p хранит тот же <адрес 1>, но лежит по <адрес 3>, а не по <адрес 2>: это другая переменная. Поэтому p = nil внутри снаружи не виден — p == nil: false.
Вызов метода на nil-указателе — всегда паника
Только если метод разыменует получатель. nt.Kind() и nt.Sum() на *Tree со значением nil отработали — "tree" и 0. Паника — у метода на значении (np.Sum() на Point) и при обращении к полю.
Разыменование nil всегда падает сразу
По спецификации — да. Но go1.24.7 мог перенести проверку в ветку, где прочитанное значение используется, и на другом пути пропустить её: f(nil, false) вернул 5. На go1.25.1 — паника. Полагаться на «не упало» нельзя.
Указатель на элемент среза всегда указывает в срез
Только пока массив не перевыделен. После append в срез с cap 3 запись *e = 100 ушла в старый массив: s[0] = 10, *e = 100. Ошибки при этом нет.
После перевыделения указатель на старый элемент недействителен, как в C
Он действителен: читать и писать через него законно. Он только потерял связь с текущим массивом среза — и удерживает старый массив в памяти: в прогоне живая куча 18 МБ, пока жив указатель, и 10 МБ без него.
Возвращать указатель на локальную переменную нельзя, как в C
Можно: переменная живёт, пока на неё есть указатель. Будет ли выделение, решает компилятор в месте вызова: newCounter() с //go:noinline — 1 выделение на вызов и moved to heap: c, а встроенная копия, чей указатель не ушёл наружу, — 0.
new(T) выделяет в куче, а &T{} — на стеке (или наоборот)
Для Point компилятор принимает для них одинаковые решения: оба escapes to heap, когда указатель возвращается, и оба does not escape, когда нет. Где окажется переменная, решает escape-анализ, а не способ записи.
new(T) и &T{} — всегда одно и то же
Для структуры — да. Для среза и карты — нет: *new(map[string]int) — nil, и запись в неё — паника assignment to entry in nil map; &map[string]int{} даёт готовую к записи карту.
Передавать по указателю всегда быстрее
Передать готовую большую структуру — в нашем замере да. Но вернуть указатель на новую структуру там же оказалось дороже, чем вернуть её значением: 301,79 нс с выделением 1024 байт против 25,62 нс без выделений (go1.24.7 linux/amd64, лучший из 11 кругов). На 16 байтах разницы при передаче почти не было. На другой машине числа будут другими — их меряют.
История версий
Замеры урока сняты на go1.24.7 и go1.25.1, linux/amd64; правила языка сверены с их спецификациями 14.10.2026. На эту дату вышли и более новые версии; из их release notes к уроку относится одно изменение в Go 1.26 — оно в последней строке таблицы, и урок его не измерял: Go 1.26 в окружении замеров нет.
| Версия | Изменение | Что это значит для кода |
|---|---|---|
| go1.24.7 | Компилятор может перенести проверку на nil в ветку, где прочитанное значение используется, и на другом пути она не выполняется: f(nil, false) из issue72860.go вернул 5, file.Name() на nil прошёл без паники. Проверено прогоном bench/goptr/nilcheck.go. | код, трогающий результат до проверки ошибки, может не падать |
| go1.25.1 | Оба случая — паника runtime error: invalid memory address or nil pointer dereference, как требует спецификация. Проход tighten больше не переносит проверки на nil (ssa/tighten.go, строки 48–49), в дистрибутиве появился регрессионный тест test/fixedbugs/issue72860.go. | такой код падает: сначала проверка ошибки, потом результат |
| Go 1.26 | По release notes: The built-in new function, which creates a new variable, now allows its operand to be an expression, specifying the initial value of the variable (встроенная функция new, создающая новую переменную, теперь допускает в качестве операнда выражение, задающее начальное значение переменной). Не измерено: у нас go1.24.7 и go1.25.1, и на них new(42) не собирается — 42 is not a type (bench/goptr/compile.sh, случай newexpr). | new(выражение) — с Go 1.26; на более старых версиях — ошибка компиляции |
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
func reset(p *int) { p = nil }
func set(p *int) { *p = 7 }
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)Практика · оцените
Проверка знаний
func drop(p *int) { p = nil }. Вызывающий передаёт p := &x и после drop(p) проверяет p == nil. Что получится?
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Указатель — это значение, в котором лежит адрес переменной.
&xберёт адрес,*pдаёт переменную по адресу, нулевое значение указателя —nil. Обращаться к полю и вызывать метод можно без звёздочки:p.X— это(*p).X, аv.Move(5)у метода на*Point— это(&v).Move(5).new(T)и&T{}дают указатель на новую переменную; для структуры это одно и то же, а для среза и карты — нет:newдаёт указатель наnil-срез илиnil-карту,&T{}— на пустой, но готовый к записи контейнер. - В Go всё передаётся по значению, и указатель тоже: функция получает копию адреса. Прогон показывает это меткой адреса: снаружи
p = <адрес 1>,&p = <адрес 2>, внутри функцииp = <адрес 1>,&p = <адрес 3>— значение то же, переменная другая. Поэтому*p = 42внутри видно снаружи, аp = &yиp = nil— нет. Общая у вызывающего и функции не переменная-указатель, а переменная, на которую он указывает. - Дальше — то, что отличает понимание от пересказа. Разыменование
nil— паникаruntime error: invalid memory address or nil pointer dereference, но метод на nil-получателе законен и отработал, пока не тронул поля. Адрес элемента карты взять нельзя — ошибка компиляции, — а элемента среза можно, и послеappend, который перевыделил массив, такой указатель теряет связь с текущим массивом среза: запись через него законна, но уходит в старый массив —s[0] = 10, *e = 100, — и старый массив живёт, пока жив указатель. Указатель на локальную переменную вернуть законно: переменная живёт, пока на неё есть указатель, а где её разместить, решает компилятор — для отдельно скомпилированной функции он пишетmoved to heap: c(переносит в кучу — память, которая переживает вызов). Арифметики указателей нет:p + 1не собирается, обходной путь только черезunsafe. А цена — наблюдение одной конфигурации, go1.24.7 linux/amd64: передать килобайт по указателю оказалось дешевле, чем по значению, но функция, вернувшая указатель на свою новую переменную, — дороже той, что вернула ту же структуру значением, из-за выделения в куче. Числа — в «Глубже»; на своей машине их надо мерить.
На самом деле
- Всё передаётся по значению; у указателя значение — адрес. В прогоне внутри функции
pхранит тот же<адрес 1>, но лежит по<адрес 3>, а не по<адрес 2>: это другая переменная. Поэтомуp = nilвнутри снаружи не виден —p == nil: false. - Только если метод разыменует получатель.
nt.Kind()иnt.Sum()на*Treeсо значениемnilотработали —"tree"и0. Паника — у метода на значении (np.Sum()наPoint) и при обращении к полю. - По спецификации — да. Но go1.24.7 мог перенести проверку в ветку, где прочитанное значение используется, и на другом пути пропустить её:
f(nil, false)вернул5. На go1.25.1 — паника. Полагаться на «не упало» нельзя. - Только пока массив не перевыделен. После
appendв срез сcap 3запись*e = 100ушла в старый массив:s[0] = 10, *e = 100. Ошибки при этом нет. - Он действителен: читать и писать через него законно. Он только потерял связь с текущим массивом среза — и удерживает старый массив в памяти: в прогоне живая куча 18 МБ, пока жив указатель, и 10 МБ без него.
- Можно: переменная живёт, пока на неё есть указатель. Будет ли выделение, решает компилятор в месте вызова:
newCounter()с//go:noinline— 1 выделение на вызов иmoved to heap: c, а встроенная копия, чей указатель не ушёл наружу, — 0. - Для
Pointкомпилятор принимает для них одинаковые решения: обаescapes to heap, когда указатель возвращается, и обаdoes not escape, когда нет. Где окажется переменная, решает escape-анализ, а не способ записи. - Для структуры — да. Для среза и карты — нет:
*new(map[string]int)—nil, и запись в неё — паникаassignment to entry in nil map;&map[string]int{}даёт готовую к записи карту. - Передать готовую большую структуру — в нашем замере да. Но вернуть указатель на новую структуру там же оказалось дороже, чем вернуть её значением: 301,79 нс с выделением 1024 байт против 25,62 нс без выделений (go1.24.7 linux/amd64, лучший из 11 кругов). На 16 байтах разницы при передаче почти не было. На другой машине числа будут другими — их меряют.
По версиям
- go1.24.7
- Компилятор может перенести проверку на
nilв ветку, где прочитанное значение используется, и на другом пути она не выполняется:f(nil, false)изissue72860.goвернул5,file.Name()наnilпрошёл без паники. Проверено прогономbench/goptr/nilcheck.go.< - go1.25.1
- Оба случая — паника
runtime error: invalid memory address or nil pointer dereference, как требует спецификация. Проходtightenбольше не переносит проверки наnil(ssa/tighten.go, строки 48–49), в дистрибутиве появился регрессионный тестtest/fixedbugs/issue72860.go.< - Go 1.26
- По release notes: The built-in new function, which creates a new variable, now allows its operand to be an expression, specifying the initial value of the variable. Не измерено: у нас go1.24.7 и go1.25.1, и на них
new(42)не собирается —42 is not a type(bench/goptr/compile.sh, случайnewexpr).<
Что разобрано
- Что здесь на самом деле спрашивают
- База: адрес переменной и переменная по адресу
- Механизм 1: параметр-указатель — копия
- Механизм 2: nil — паника при разыменовании, но не при вызове метода
- Механизм 3: адрес есть у переменной — у элемента среза есть, у элемента карты нет
- Механизм 4: указатель на локальную переменную — законно
- Механизм 5: арифметики указателей нет — только через unsafe
- Глубже: значение или указатель — цена копии против цены кучи
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- История версий
- Практика
- Проверка знаний
Источники и что читать дальше
10 ИСТОЧНИКОВ
- Спецификация Go — Pointer types и Comparison operatorsОфициальная документация. Что такое указатель: «A pointer type denotes the set of all pointers to variables of a given type, called the base type of the pointer. The value of an uninitialized pointer is nil» (Тип-указатель обозначает множество всех указателей на переменные данного типа, называемого базовым типом указателя. Значение неинициализированного указателя — nil). Когда указатели равны: «Two pointer values are equal if they point to the same variable or if both have value nil. Pointers to distinct zero-size variables may or may not be equal» (Два указателя равны, если указывают на одну и ту же переменную или оба равны nil. Указатели на разные переменные нулевого размера могут быть равны, а могут и не быть). Сверено по doc/go_spec.html дистрибутивов go1.24.7 и go1.25.1.https://go.dev/ref/spec#Pointer_types
- Спецификация Go — CallsОфициальная документация. Почему параметр — копия: «After they are evaluated, new storage is allocated for the function's variables, which includes its parameters and results. Then, the arguments of the call are passed to the function, which means that they are assigned to their corresponding function parameters, and the called function begins execution» (После вычисления выделяется память под переменные функции, включая её параметры и результаты. Затем аргументы вызова передаются функции — то есть присваиваются соответствующим параметрам, — и вызванная функция начинает выполняться). И вызов метода без амперсанда: «If x is addressable and &x's method set contains m, x.m() is shorthand for (&x).m()» (Если x адресуема и набор методов &x содержит m, то x.m() — сокращение для (&x).m()).https://go.dev/ref/spec#Calls
- Спецификация Go — Address operatorsОфициальная документация. У чего есть адрес: «The operand must be addressable, that is, either a variable, pointer indirection, or slice indexing operation; or a field selector of an addressable struct operand; or an array indexing operation of an addressable array» (Операнд должен быть адресуемым, то есть быть переменной, разыменованием указателя или индексированием среза; либо селектором поля адресуемой структуры; либо индексированием адресуемого массива). Исключение: «As an exception to the addressability requirement, x may also be a (possibly parenthesized) composite literal» (В виде исключения из требования адресуемости x может быть и составным литералом, возможно в скобках). Разыменование nil: «For an operand x of pointer type *T, the pointer indirection *x denotes the variable of type T pointed to by x. If x is nil, an attempt to evaluate *x will cause a run-time panic» (Для операнда x типа *T разыменование *x обозначает переменную типа T, на которую указывает x. Если x равен nil, попытка вычислить *x вызывает панику во время выполнения).https://go.dev/ref/spec#Address_operators
- Спецификация Go — SelectorsОфициальная документация. Откуда p.X без звёздочки: «For a value x of type T or *T where T is not a pointer or interface type, x.f denotes the field or method at the shallowest depth in T where there is such an f» (Для значения x типа T или *T, где T — не указатель и не интерфейс, x.f обозначает поле или метод на наименьшей глубине в T, где такое f есть). И nil: «If x is of pointer type and has the value nil and x.f denotes a struct field, assigning to or evaluating x.f causes a run-time panic» (Если x — указатель со значением nil и x.f обозначает поле структуры, присваивание x.f или его вычисление вызывает панику во время выполнения).https://go.dev/ref/spec#Selectors
- Спецификация Go — Variables, Composite literals, AllocationОфициальная документация. Что общего у new(T) и &T{}: «Calling the built-in function new or taking the address of a composite literal allocates storage for a variable at run time» (Вызов встроенной функции new или взятие адреса составного литерала выделяет память под переменную во время выполнения). Про литерал: «Taking the address of a composite literal generates a pointer to a unique variable initialized with the literal's value» (Взятие адреса составного литерала порождает указатель на уникальную переменную, инициализированную значением литерала). Про new: «The built-in function new takes a type T, allocates storage for a variable of that type at run time, and returns a value of type *T pointing to it» (Встроенная функция new принимает тип T, выделяет во время выполнения память под переменную этого типа и возвращает значение типа *T, указывающее на неё). И где формы расходятся: «Note that the zero value for a slice or map type is not the same as an initialized but empty value of the same type» (Заметьте, что нулевое значение типа среза или карты — не то же самое, что инициализированное, но пустое значение того же типа).https://go.dev/ref/spec#Allocation
- Go 1.26 Release Notes — new с выражениемОфициальная документация. «The built-in
newfunction, which creates a new variable, now allows its operand to be an expression, specifying the initial value of the variable» (Встроенная функция new, создающая новую переменную, теперь допускает в качестве операнда выражение, задающее начальное значение переменной). Урок это не измерял: Go 1.26 в окружении замеров нет; на go1.24.7 и go1.25.1 new(42) не собирается (bench/goptr/compile.sh, случай newexpr). Сверено 14.10.2026.https://go.dev/doc/go1.26 - Пакет unsafe — Pointer и AddОфициальная документация. Чем платят за арифметику: «Pointer therefore allows a program to defeat the type system and read and write arbitrary memory. It should be used with extreme care» (Поэтому Pointer позволяет программе обойти систему типов и читать и писать произвольную память. Пользоваться им нужно с крайней осторожностью). И сама арифметика: «The function Add adds len to ptr and returns the updated pointer Pointer(uintptr(ptr) + uintptr(len))» (Функция Add прибавляет len к ptr и возвращает обновлённый указатель Pointer(uintptr(ptr) + uintptr(len))). Сверено по go doc unsafe.Pointer и go doc unsafe.Add, go1.24.7.https://pkg.go.dev/unsafe#Pointer
- runtime/panic.go и runtime/error.go — текст паники при разыменовании nilИсходный код Go. Строка 258 runtime/panic.go на go1.24.7 и go1.25.1:
var memoryError = error(errorString("invalid memory address or nil pointer dereference")); строки 260–263 —panicmem, которая паникует этим значением. Префикс «runtime error: » добавляет методerrorString.Error— runtime/error.go, строки 81–82 на go1.24.7 (91–92 на go1.25.1). К интерфейсу runtime.Error значение относит не текст, а методRuntimeError()— строка 79 на go1.24.7 (89 на go1.25.1); в go doc runtime.Error: «a type is a run time error if it has a RuntimeError method» (тип считается ошибкой времени выполнения, если у него есть метод RuntimeError).https://github.com/golang/go/blob/go1.24.7/src/runtime/panic.go#L258-L263 - test/fixedbugs/issue72860.go — проверка на nil, которую компилятор пропускалИсходный код Go. Регрессионный тест из дистрибутива go1.25.1; в дистрибутиве go1.24.7 такого файла нет. Функция f читает *p в valid и выходит по !b, не глядя на valid; тест требует паники при f(nil, false). Прогон
bench/goptr/nilcheck.go: на go1.24.7 вызов вернул 5, на go1.25.1 — паника.https://github.com/golang/go/blob/go1.25.1/test/fixedbugs/issue72860.go - cmd/compile/internal/ssa/tighten.go — где оставаться проверке на nilИсходный код Go. Проход tighten, строка 9: «tighten moves Values closer to the Blocks in which they are used» (tighten переносит значения ближе к блокам, где они используются). В go1.25.1 в строках 48–49 добавлено исключение: «Nil checks need to stay in their block. See issue 72860» (Проверки на nil должны оставаться в своём блоке. См. issue 72860). В tighten.go дистрибутива go1.24.7 этого исключения нет.https://github.com/golang/go/blob/go1.25.1/src/cmd/compile/internal/ssa/tighten.go#L48-L49