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

ОСНОВЫ ЯЗЫКА: GO Указатели

Обязательно

Указатели в Go: & и *, значения и ссылки

Go

Указатели в Go простыми словами: что делают & и *, зачем нужны, когда передавать значение, а когда указатель, что такое nil и почему нет арифметики указателей.

1. По умолчанию функция получает копию

1 / 5

Переданное число или маленькая структура копируется. Изменение копии не меняет оригинал, поэтому обычные значения безопаснее и проще.

  • setToZero(n) меняет только локальную копию x.
  • После вызова исходная переменная остаётся прежней.
  • Когда нужен оригинал, передают его адрес.
n = 42оригинал копия x = 0изменённая копия n остаётся 42

2. & берёт адрес, * возвращает значение по адресу

2 / 5

Указатель хранит адрес, а не значение. Оператор &n создаёт *int, а *p читает или меняет значение по этому адресу.

  • В типе *int звёздочка означает «указатель на int».
  • В выражении *p - разыменование.
  • *p = 100 меняет исходную n.
n = 42адрес 0xc000 p = &nтот же адрес *p = 42 Указатель - одно машинное слово

3. Указатель нужен для изменения и больших структур

3 / 5

Если функция меняет аргумент, передают указатель. Маленькие неизменяемые значения передают по значению. Срезы и map уже ссылаются на общие данные.

  • incValue(c) меняет копию Counter.
  • incPointer(&c) меняет оригинал.
  • Для поля структуры c.value++ Go сам разыменовывает указатель.
Countervalue копия = 0 оригинал = 1 Меняем состояние - pointer, только читаем - value

4. nil нельзя разыменовывать

4 / 5

Нулевое значение указателя - nil. Перед разыменованием проверяют p != nil, иначе будет panic.

  • var p *int создаёт nil-указатель.
  • new(int) или &T{} возвращает указатель на новое нулевое значение.
  • Метод на nil-указателе вызвать можно, если сам метод проверяет nil.
var p *intnil p != nil? безопасно new(int)*p = 0 Без проверки - panic

5. Нет арифметики указателей и адресации map

5 / 5

В Go нельзя делать p + 1 или переходить по памяти как в C. Для обхода коллекций используют срезы и range.

  • Элемент map не адресуем: &m["a"] не соберётся.
  • Меняют копию структуры и кладут обратно или хранят map[string]*Point.
  • Указатели сравнивают с другими указателями или nil, не с числами.
p + 1 &m["a"] copy -> change -> put back Обход - range, не адресная арифметика

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

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

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

живой пример

package main

import "fmt"

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

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

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

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

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

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

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

В Go всего два оператора для работы с указателями. Сам указатель это адрес в памяти, одно машинное слово: 8 байт на 64-битной платформе независимо от того, на что он указывает.

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

живой пример

package main

import "fmt"

func main() {
    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) это операция разыменования. Это разные роли одного символа.

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

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

живой пример

package main

import "fmt"

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

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

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

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

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

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

живой пример

package main

import "fmt"

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) → обычно передают по значению: внутри они и так ссылаются на общие данные (подробнее — в статье про срезы и карты). Оговорка про срез: функция увидит и изменит его элементы, но append внутри неё не удлинит срез у вызывающего — новый срез возвращают результатом.

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

nil-указатели

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

живой пример

package main

import "fmt"

func main() {
    var p *int     // объявлен, но не инициализирован
    fmt.Println(p) // <nil>
}
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

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

живой пример

package main

import "fmt"

func main() {
    defer func() {
        if recover() != nil {
            fmt.Println("panic: invalid memory address or nil pointer dereference")
        }
    }()
    var p *int
    fmt.Println(*p) // паника: разыменование nil
}
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

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

живой пример

package main

import "fmt"

func main() {
    n := 5
    p := &n
    if p != nil {
        fmt.Println(*p) // 5
    }
    var q *int
    if q != nil {
        fmt.Println(*q)
    } else {
        fmt.Println("q равно nil — идти нельзя")
    }
}
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

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

живой пример

package main

import "fmt"

func main() {
    p := new(int) // выделяет int со значением 0 и возвращает *int
    fmt.Println(*p) // 0
    *p = 7
    fmt.Println(*p) // 7
}
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

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

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

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

Дополнительно: при первом чтении можно пропустить

Глубже: чего нельзя адресовать и метод на nil-указателерасширенное

Адрес берут не у всего. Элемент map не адресуем: &m["a"] не соберётся, а m["a"].X = 5 для map со структурами компилятор отвергнет — cannot assign to struct field m["a"].X in map. Причина в том, что map при росте перекладывает элементы, и указатель внутрь неё жил бы недолго. Выход — достать значение, изменить и положить обратно, либо хранить в map указатели: map[string]*Point.

new(T) и &T{} дают одно и то же — указатель на свежее нулевое значение; в коде чаще встречается вторая форма, потому что в неё сразу можно вписать поля.

И наоборот, указатель, равный nil, — не всегда авария. Метод с pointer receiver можно вызвать на nil-указателе: panic не будет, пока метод не полезет в поля. Этим пользуются, чтобы nil-значение типа было безопасным — например, var l *Logger; l.Print("x"), где Print проверяет l == nil и молча выходит.

Коротко

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

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

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