Указатели в Go: на что смотреть новичку
Указатели в Go проще, чем в C, но всё равно вызывают у новичков одни и те же ошибки: nil dereference, путаница между значением и адресом, ненужное копирование структур. Разбираем, как устроены указатели, зачем они нужны и где вы точно споткнётесь.

Зачем вообще указатели, если есть значения
В Go каждая переменная — это ячейка памяти. Когда вы передаёте структуру в функцию, Go копирует её целиком. Для User с десятком полей это десятки байт на каждый вызов. Указатель — это адрес ячейки, всегда 8 байт на 64-битной платформе. Передавая указатель, вы передаёте адрес, а не копию. Это экономит память и, что важнее, позволяет функции изменить оригинал, а не его клон.
В отличие от C и C++, Go не даёт арифметику указателей1. Нельзя прибавить единицу к указателю и шагнуть на следующий элемент массива. Это убирает целый класс багов с выходом за границы памяти, но и лишает привычных трюков. Всё, что вы можете сделать с указателем в Go, — это разыменовать его (получить значение) или взять адрес переменной.
Синтаксис: три оператора, которые нужно запомнить
Весь синтаксис укладывается в три конструкции:
&x— взять адрес переменнойx.*p— разыменовать указательp, то есть получить значение, на которое он указывает.*T— тип «указатель на T». Например,*int— указатель на целое число.
Вот минимальный пример (Go 1.21+):
package main
import "fmt"
func double(val *int) {
// Разыменовываем указатель и удваиваем значение по адресу
*val = *val * 2
}
func main() {
n := 10
fmt.Println("до:", n) // до: 10
double(&n) // передаём адрес переменной n
fmt.Println("после:", n) // после: 20
}
Если убрать & и передать просто n, компилятор не пропустит код — типы не совпадут. Это одна из причин, почему указатели в Go безопаснее, чем в C: система типов не даст перепутать значение и адрес молча.
Значение или указатель: когда что передавать
Главный вопрос новичка — «когда передавать по значению, а когда по указателю?». Универсального правила нет, но есть набор критериев, по которым решение принимается быстро.
| Критерий | По значению | По указателю |
|---|---|---|
| Размер структуры | Маленькая (до 2–3 полей, примитивы) | Большая (десятки полей, вложенные слайсы) |
| Нужно ли менять оригинал | Нет, работаем с копией | Да, функция модифицирует исходные данные |
| Семантика nil | nil невозможен — значение всегда инициализировано | nil допустим — «значения нет» |
| Безопасность при конкурентности | Копия изолирована от других горутин | Нужна синхронизация, если читают и пишут одновременно |
| Escape-анализ | Чаще остаётся на стеке | Указатель может «убежать» в кучу — сборщик мусора получит работу |
Практическое правило из стандартной библиотеки Go: методы, которые меняют состояние приёмника, используют указательный ресивер (s *Server). Методы, которые только читают, — тоже часто указательный, чтобы не копировать тяжёлую структуру. Значимый ресивер ((s Server)) встречается у маленьких иммутабельных типов вроде time.Time.
Пять типичных ошибок
1. Разыменование nil-указателя
Самая частая паника у новичков — runtime error: invalid memory address or nil pointer dereference. Если указатель объявлен, но не инициализирован, его значение — nil. Любая попытка прочитать или записать по нему приведёт к панике.
var p *int
fmt.Println(*p) // паника: nil pointer dereference
Защита простая — проверяйте на nil до разыменования:
if p != nil {
fmt.Println(*p)
}
2. Возврат адреса локальной переменной — и напрасный страх
В C это грубая ошибка: локальная переменная живёт на стеке и после выхода из функции адрес становится мусором. В Go компилятор выполняет escape-анализ: если переменная «убегает» через возвращаемый указатель, она размещается в куче, а не на стеке. Код ниже абсолютно корректен:
func newCounter() *int {
c := 0
return &c // безопасно: Go разместит c в куче
}
Новички, пришедшие из C, боятся так делать. В Go это идиома — пользуйтесь.
3. Путаница с указательным и значимым ресивером
Если у типа часть методов с ресивером *T, а часть с T, вы получите неожиданное поведение: метод с ресивером T работает с копией. Стандартный совет — не смешивайте. Если хоть один метод должен менять состояние, делайте все ресиверы указательными.
4. Передача указателя на элемент слайса после append
Слайс при росте переаллоцирует внутренний массив. Указатель, взятый до append, может смотреть на старую память:
s := []int{1, 2, 3}
p := &s[0]
s = append(s, 4, 5, 6, 7, 8) // возможна переаллокация
*p = 99
fmt.Println(s[0]) // может напечатать 1, а не 99
Если слайс не перерос свою ёмкость, массив останется тем же и указатель будет валиден. Но полагаться на это — источник плавающих багов.
5. Указатели и горутины без синхронизации
Передать указатель в горутину просто. Забыть, что две горутины пишут по одному адресу, — ещё проще. Детектор гонок (go run -race) ловит такие ситуации, но только если код исполняется в тесте. Подробности конкурентной работы с общей памятью — отдельная тема; здесь достаточно запомнить: указатель, переданный в горутину, нуждается в мьютексе или канале.
Указатели в Go — это не про оптимизацию. Это про семантику: хотите ли вы, чтобы вызываемая функция видела и меняла ваш объект, или она должна работать с собственной копией. Выбирайте исходя из намерения, а не из размера структуры.
new и & — в чём разница
Go предлагает два способа получить указатель на свежее значение:
p1 := new(int) // *int, указывает на 0
p2 := &int{} // не скомпилируется — нельзя взять адрес литерала примитива
type Config struct{ Port int }
c1 := new(Config) // *Config, все поля — zero value
c2 := &Config{Port: 8080} // *Config с заполненным полем
new(T) возвращает *T, заполненный нулевым значением. Литерал &T{...} делает то же самое, но позволяет сразу задать поля. На практике new в Go-коде встречается редко — &T{} выразительнее.
Escape-анализ: куда уходит переменная
Компилятор решает, где жить переменной — на стеке или в куче. Если указатель на неё покидает функцию, переменная «убегает» в кучу. Это не ошибка, но это нагрузка на сборщик мусора. Посмотреть решения компилятора можно флагом сборки:
go build -gcflags="-m" main.go
Вывод покажет строки вида moved to heap: c. В горячих циклах escape в кучу — измеримая проблема; в обычном CRUD-сервисе — вряд ли. Не оптимизируйте вслепую: сначала бенчмарк (go test -bench), потом решение.
Что делать дальше
После того как базовый синтаксис указателей стал понятен, стоит разобраться в трёх связанных темах:
- Интерфейсы и указатели. Почему значение типа
Tне реализует интерфейс, если методы определены на*T. Это частый вопрос на код-ревью. - Пакет
unsafe. Он существует, но в прикладном коде почти никогда не нужен. Знать о нём стоит, использовать — нет, пока не упрётесь в конкретную задачу FFI или низкоуровневой сериализации. - Профилирование аллокаций.
go tool pprofпокажет, где ваш код порождает лишние аллокации в куче — и нужны ли указатели там, где вы их поставили.
Лучший способ закрепить тему — написать тип с несколькими методами, часть из которых меняет состояние, и прогнать детектор гонок. Это покажет указатели в деле, а не в учебнике.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: Go Specification — Pointer types, Effective Go — Pointers vs. Values
Частые вопросы
Можно ли делать арифметику указателей в Go?
Безопасно ли возвращать указатель на локальную переменную?
Когда использовать new, а когда &T{}?
Если у меня другая версия Go, примеры будут работать?
Указатели замедляют сборщик мусора?
Что будет, если передать указатель в горутину без синхронизации?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.