← назад к разделу

Указатель — это переменная, которая хранит не само значение, а адрес другого значения в памяти. Звучит страшно, но на практике в Go это всего две операции и пара простых правил. Разберём с нуля: зачем они вообще нужны и когда их применять.

Зачем нужны указатели

По умолчанию Go копирует значения. Когда вы передаёте переменную в функцию, функция получает её копию — и любые изменения внутри функции остаются внутри функции:

func setToZero(x int) {
    x = 0 // меняем копию, оригинал не тронут
}

func main() {
    n := 42
    setToZero(n)
    fmt.Println(n) // 42 — ничего не изменилось
}

Указатели решают две задачи:

  • дать функции изменить оригинал, а не его копию;
  • избежать копирования больших значений (структур), когда копия дорого обходится.

Короткая формула: указатель — это «адрес, по которому лежит значение», а не само значение.

Две операции: & и *

В Go всего два оператора для работы с указателями.

& — взять адрес. Ставится перед переменной и возвращает указатель на неё:

n := 42
p := &n            // p — указатель на n, тип *int
fmt.Println(p)     // что-то вроде 0xc000012345 (адрес)

* — разыменовать. Ставится перед указателем и возвращает (или меняет) то значение, на которое он указывает:

fmt.Println(*p)    // 42 — значение по адресу
*p = 100           // меняем значение по адресу
fmt.Println(n)     // 100 — изменился сам n!

Обратите внимание: * живёт в двух местах. В типе (*int, *User) это значит «указатель на такой тип». В выражении (*p) это операция разыменования. Это разные роли одного символа.

Изменение оригинала через указатель

Вернёмся к первому примеру, но теперь функция принимает указатель:

func setToZero(x *int) {
    *x = 0 // меняем значение по адресу — то есть оригинал
}

func main() {
    n := 42
    setToZero(&n)  // передаём адрес n
    fmt.Println(n) // 0 — оригинал изменился
}

Это самый частый повод взять указатель: функция должна поменять то, что ей передали.

Value vs reference семантика

С обычными значениями (число, строка, маленькая структура) работает семантика значений (value semantics): копия независима от оригинала. С указателями — ссылочная семантика (reference semantics): через копию указателя вы дотягиваетесь до того же самого значения в памяти.

type Counter struct {
    value int
}

func incValue(c Counter)  { c.value++ } // меняет копию
func incPointer(c *Counter) { c.value++ } // меняет оригинал

func main() {
    c := Counter{}
    incValue(c)
    fmt.Println(c.value) // 0 — копия

    incPointer(&c)
    fmt.Println(c.value) // 1 — оригинал
}

Заметьте: внутри incPointer мы написали c.value++, а не (*c).value++. Go сам разыменовывает указатель при обращении к полю структуры — это синтаксическое удобство, явная звёздочка здесь не нужна.

Когда указатель, а когда значение

Простые ориентиры для начала:

  • Нужно изменить аргумент внутри функции → передавайте указатель.
  • Структура большая и копировать её на каждый вызов накладно → указатель.
  • Значение маленькое и неизменяемое (число, короткая строка, маленькая структура) → передавайте по значению, так проще и безопаснее.
  • Тип уже ссылочный (slice, map, chan) → обычно передают по значению: внутри они и так ссылаются на общие данные (подробнее — в статье про срезы и карты).

Не гонитесь за указателями «ради скорости» везде. Для мелких значений копия часто быстрее и понятнее, чем работа через адрес.

nil-указатели

Указатель, который никуда не указывает, имеет значение nil — это его «нулевое» состояние по умолчанию:

var p *int       // объявлен, но не инициализирован
fmt.Println(p)   // <nil>

Попытка разыменовать nil-указатель приводит к панике (аварийному завершению):

var p *int
fmt.Println(*p)  // panic: runtime error: invalid memory address or nil pointer dereference

Поэтому перед разыменованием, когда указатель может быть пустым, его проверяют:

if p != nil {
    fmt.Println(*p)
}

Кстати, когда вам нужен новый указатель на свежее значение, удобно использовать new:

p := new(int) // выделяет int со значением 0 и возвращает *int
*p = 7

Нет арифметики указателей

Если вы видели C, то помните трюки вида p + 1 для перехода к следующему элементу в памяти. В Go этого нет. Указатель нельзя складывать, вычитать или сравнивать с числом — только присваивать, разыменовывать и сравнивать с другим указателем (или с nil).

Это сделано намеренно: ради безопасности памяти. Невозможность «гулять» по адресам убирает целый класс ошибок, а для перебора элементов в Go есть срезы и range — безопасные и удобные (см. статью про срезы и карты).

Коротко

  • Указатель хранит адрес значения, а не само значение; его тип — *T.
  • &x берёт адрес переменной, *p разыменовывает указатель (читает или меняет значение по адресу).
  • По умолчанию Go копирует; указатель нужен, чтобы функция изменила оригинал или чтобы не копировать большую структуру.
  • Для мелких неизменяемых значений передавайте по значению — это проще и безопаснее.
  • Нулевое значение указателя — nil; разыменование nil вызывает панику, поэтому проверяйте перед использованием.
  • Арифметики указателей в Go нет — это осознанный выбор ради безопасности памяти.

Что почитать дальше

  • Структуры и методы — где указатели нужны чаще всего: receiver-методы и изменение полей.
  • Срезы и карты — ссылочные типы, которые ведут себя «как указатели», но передаются по значению.
  • Синтаксис и типы — базовые типы и нулевые значения, на которые опирается эта статья.