Указатель — это переменная, которая хранит не само значение, а адрес другого значения в памяти. Звучит страшно, но на практике в 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-методы и изменение полей.
- Срезы и карты — ссылочные типы, которые ведут себя «как указатели», но передаются по значению.
- Синтаксис и типы — базовые типы и нулевые значения, на которые опирается эта статья.