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