& и *
Указатель хранит адрес значения в памяти. &x даёт указатель на x. *p переходит по указателю к значению.
У звёздочки две роли. В типе (*int) она означает «указатель на int». В выражении (*p) она означает «значение, на которое указывает p». Если напечатать сам p, получится адрес вроде 0xc000012345, который меняется от запуска к запуску.
Зачем нужны указатели: общий доступ вместо копирования
Go передаёт всё по значению. Функция получает копию каждого аргумента, поэтому не может изменить переменную вызывающего кода. Передача указателя копирует адрес, и обе стороны добираются до одного и того же значения.
Ссылок в смысле C++ в Go нет. «Передача по ссылке» в Go означает передачу указателя по значению.
Указатели на структуры
Указатели на структуры это самые частые указатели в коде на Go. Обращение к полю разыменовывает автоматически: p.Name означает (*p).Name.
Методы подчиняются той же логике: получатель-указатель позволяет методу менять структуру. Выбор между получателем-значением и получателем-указателем разобран на странице о методах.
new
new(T) выделяет обнулённое значение T и возвращает его адрес. Это то же самое, что объявить переменную и взять её адрес.
p := new(int) // *int pointing at 0
var x int
q := &x // same thing, two lines
a := new(Account) // same as &Account{}
На практике для структур чаще пишут &T{...}, потому что так можно задать поля в том же выражении. new удобен для указателей на базовые типы. Не путайте его с make, который создаёт слайсы, мапы и каналы и возвращает их готовыми к работе, а не указателями.
Возвращать указатель на локальную переменную безопасно
В C возврат адреса локальной переменной это баг. В Go это обычный код.
Где будет жить значение, в стеке или в куче, решает escape-анализ компилятора. Вы этого никогда не выбираете и никогда не освобождаете память: сборщик мусора забирает её, когда на неё больше ничего не указывает. go build -gcflags=-m печатает эти решения, если интересно.
nil-указатели
Нулевое значение любого типа указателя это nil. Разыменование nil вызывает панику.
Вывод:
1
2
true
recovered: runtime error: invalid memory address or nil pointer dereference
Без recover программа упала бы с этим сообщением и трассировкой стека, начинающейся с panic: runtime error: invalid memory address or nil pointer dereference и [signal SIGSEGV: segmentation violation ...]. Типичные источники:
- переменная-указатель или поле структуры, которым так и не присвоили значение
m[key]вmap[string]*Tдля отсутствующего ключа, что возвращаетnil- проигнорированная ошибка:
f, _ := os.Open(path)оставляетfравным nil при неудаче - метод с получателем-указателем, вызванный на nil-указателе, который затем читает поле
Сам вызов метода на nil-указателе допустим. Паника случается, только когда метод обращается к полям получателя.
Когда использовать указатель
Используйте указатель, когда:
- функция или метод должны изменить значение вызывающего кода
- значение это большая структура, которую часто передают
- тип нельзя копировать (он содержит
sync.Mutexили представляет уникальный ресурс, например соединение) - нужно выразить «нет значения» через
nil, например необязательное поле в JSON-структуре (*intотличает «отсутствует» от0)
Не используйте указатель, когда:
- значение маленькое и только для чтения (
time.Time,Point) - тип и так ведёт себя как ссылка: слайсам, мапам, каналам, функциям и интерфейсам
*перед ними нужен редко.*[]intили*map[string]intпочти всегда ошибка
Указатели не быстрее автоматически. Копировать маленькую структуру дёшево, а указатель может вынудить выделение в куче и добавить работы сборщику мусора.
Указатели и переменные цикла
Брать адрес переменной цикла безопасно начиная с Go 1.22, потому что у каждой итерации своя переменная:
Программа печатает 0 1 2. До Go 1.22 (где также появился range 3) тот же цикл, записанный как for i := 0; i < 3; i++, печатал 3 3 3, потому что все три указателя делили одну переменную. Учтите, что &v в for _, v := range items указывает на копию элемента, а не на элемент слайса. Чтобы получить указатель на сам элемент, используйте &items[i].
Частые ошибки
- Разыменование без проверки. Проверяйте
if p == nilвезде, где возможен nil. - Указатель на значение из range.
&vуказывает на копию; используйте&s[i]. - Указатели на слайсы и мапы. Нужны редко. Лучше возвращайте новый слайс.
- Хранение
&s[i]приappend. Еслиappendперевыделит память, указатель останется на старом массиве, и записи через него не будут видны в новом слайсе.
Часто задаваемые вопросы
Что означают & и * в Go?
&x берёт адрес x и даёт указатель типа *T. *p разыменовывает указатель: читает или записывает значение, на которое он указывает. В типе *T означает «указатель на T».
Чем new отличается от make в Go?
new(T) выделяет обнулённое значение T и возвращает *T; работает с любым типом. make работает только со слайсами, мапами и каналами и возвращает инициализированное значение этого типа (не обнулённое и не указатель): make(map[string]int) сразу готова к работе, а new(map[string]int) это указатель на nil-мапу.
Можно ли вернуть указатель на локальную переменную в Go?
Да. В отличие от C, это безопасно. Escape-анализ компилятора видит, что переменная переживёт функцию, и размещает её в куче. func newInt() *int { x := 5; return &x } это корректный код на Go.
Откуда берётся «invalid memory address or nil pointer dereference» в Go?
Это чтение или запись через nil-указатель: переменная *T, которой так и не присвоили значение, поиск отсутствующего ключа в map[K]*V или функция, вернувшая nil вместе с ошибкой, которую вы не проверили. Проверяйте на nil или инициализируйте указатель перед использованием.
Есть ли в Go арифметика указателей?
Нет. К указателю нельзя прибавить число или обратиться через него к памяти по индексу. Пакет unsafe разрешает это для низкоуровневого кода, без каких-либо гарантий безопасности. Обычный код на Go использует слайсы для непрерывной памяти.