Введение
Любая переменная в программе живёт по какому-то адресу в оперативной памяти.
Обычно мы об этом не думаем: пишем x = 5 и работаем со значением.
Но рано или поздно возникает вопрос: как сделать так, чтобы функция изменила
переданную ей переменную? Или как передать в функцию большую структуру, не
копируя её целиком? Ответ на оба вопроса — указатели.
Указатель — это переменная, которая хранит не значение, а адрес
другого значения. Хорошая аналогия: значение — это дом, а указатель — бумажка
с его адресом. Держа адрес, ты можешь прийти к дому и что-то в нём поменять, при
этом сама бумажка маленькая и её легко передать другому. В этой главе разберём,
как в Go взять адрес (&), как объявить указатель
(*T), как через него добраться до значения (разыменование
*p) и почему это делает программы одновременно и мощнее, и
опаснее.
Тема указателей у многих вызывает тревогу — виной тому память о C, где
неаккуратная работа с указателями роняет программы и открывает уязвимости.
Спешу успокоить: в Go указатели намеренно сделаны безопасными.
Здесь нет арифметики указателей (нельзя прибавить к адресу число и уехать в
произвольную ячейку), нет ручного освобождения памяти (за это отвечает сборщик
мусора), а компилятор сам решает, где разместить данные, чтобы указатель не
«протух». Всё, что тебе останется помнить, — одно правило безопасности:
не разыменовывай nil. Поэтому воспринимай указатели не как источник
опасности, а как аккуратный инструмент «дать функции право менять мой объект».
В Go всё передаётся по значению
Ключевой факт, который надо усвоить раз и навсегда: в Go всё передаётся по значению. Когда ты передаёшь переменную в функцию, функция получает копию. Изменения копии не затрагивают оригинал. Это касается и обычных чисел, и структур. Указатели не отменяют это правило — они его обходят: мы копируем не сам объект, а его адрес, и по этому адресу добираемся до оригинала.
package main
import "fmt"
// zero пытается обнулить аргумент, но получает КОПИЮ.
func zero(z int) {
z = 0
}
func main() {
x := 5
zero(x)
fmt.Println(x) // 5 — оригинал не изменился
}
Почему язык устроен именно так? Передача по значению делает функции
предсказуемыми: глядя на вызов zero(x), ты по
умолчанию знаешь, что x не изменится, если только функция явно не
получила его адрес. В языках, где всё передаётся по ссылке, любой вызов
потенциально может испортить твои данные, и приходится держать это в голове.
В Go «мутация на расстоянии» всегда видна в коде — по оператору
& при вызове или по типу *T в сигнатуре. Указатели
— это осознанное разрешение «да, тебе можно менять мой объект».
Адрес памяти и оператор &
Чтобы узнать адрес переменной, перед ней ставят оператор &
(«амперсанд», «взятие адреса»). Результат — шестнадцатеричное число вроде
0xc000012028. Само по себе оно нам редко интересно, но оно —
пропуск к тому, чтобы менять переменную «на расстоянии».
package main
import "fmt"
func main() {
a := 43
fmt.Println(a) // 43 — значение
fmt.Println(&a) // 0xc0000140a0 — адрес (у тебя будет другой)
fmt.Printf("%p\n", &a) // тот же адрес через глагол %p
}
Ты уже встречал & раньше, даже не осознавая: функция
fmt.Scan принимает адрес переменной, чтобы записать в неё
введённое значение. Без & она получила бы копию и не смогла бы
ничего вернуть наружу.
var meters float64
fmt.Print("Сколько метров: ")
fmt.Scan(&meters) // передаём АДРЕС, чтобы Scan записал значение в meters
fmt.Println("проплыл", meters, "метров")
Тип указателя *T и разыменование
Если у переменной тип int, то адрес этой переменной имеет тип
*int — «указатель на int». Звёздочка здесь — часть типа. Чтобы из
указателя достать само значение, тоже используют звёздочку, но уже как оператор:
*p означает «значение, лежащее по адресу p». Эта
операция называется разыменованием (dereferencing).
package main
import "fmt"
func main() {
a := 43
var b = &a // b имеет тип *int, хранит адрес a
fmt.Println(b) // 0xc0000140a0 — адрес
fmt.Println(*b) // 43 — разыменование: значение по адресу
}
Одна и та же звёздочка играет две роли, и это путает новичков. В
объявлении типа (var b *int) звёздочка говорит «это
указатель». В выражении (*b) звёздочка означает «пойди по
адресу и возьми значение». Различай их по контексту: рядом с типом — часть типа,
перед переменной-указателем — операция.
Изменение значения через указатель
Разыменование можно поставить и слева от присваивания. Тогда мы говорим: «значение по этому адресу — измени его». Именно так функция получает возможность поменять переменную вызывающего кода.
package main
import "fmt"
func main() {
a := 43
b := &a
*b = 42 // "по адресу b запиши 42"
fmt.Println(a) // 42 — изменили a через указатель!
}
Передача по значению против передачи через указатель
Вернёмся к функции zero, которая раньше не сработала. Если
параметр объявить как *int и передать адрес, функция сможет
изменить оригинал: она получит копию адреса, но адрес-то указывает на
ту же самую ячейку памяти.
package main
import "fmt"
// Теперь zero принимает указатель и меняет значение по адресу.
func zero(z *int) {
*z = 0
}
func main() {
x := 5
zero(&x) // передаём адрес x
fmt.Println(x) // 0 — теперь оригинал изменился
}
Сравни две версии в одном примере, чтобы почувствовать разницу: одна и та же
логика, но *int даёт эффект, а int — нет.
func byValue(v int) { v = 100 } // меняет копию
func byPointer(v *int) { *v = 100 } // меняет оригинал
func main() {
n := 1
byValue(n)
fmt.Println(n) // 1
byPointer(&n)
fmt.Println(n) // 100
}
Зачем указатели нужны
Две главные причины. Первая — изменяемость: если функция должна поменять переданные данные (заполнить структуру, обнулить поле), ей нужен указатель. Вторая — производительность: большие структуры дорого копировать при каждом вызове. Передав указатель (несколько байт адреса), мы избегаем копирования всего объекта. На этом же основаны методы с указательным получателем, которые ты встретишь в главе про структуры.
type Account struct {
Owner string
Balance int
}
// deposit меняет баланс оригинала — нужен указатель.
func deposit(a *Account, amount int) {
a.Balance += amount // Go сам разыменует: (*a).Balance
}
func main() {
acc := Account{Owner: "Аня", Balance: 100}
deposit(&acc, 50)
fmt.Println(acc.Balance) // 150
}
Небольшое удобство: при доступе к полю структуры через указатель писать
(*a).Balance не обязательно — Go автоматически разыменует, и можно
писать просто a.Balance. Это синтаксический сахар, экономящий
скобки.
Стек, куча и escape-анализ
Возникает резонный вопрос: если указатель на локальную переменную можно вернуть из функции, не «протухнет» ли он, ведь локальные переменные вроде бы живут на стеке и исчезают при выходе? В C это была бы классическая ошибка — висячий указатель. Но Go делает escape-анализ: компилятор сам определяет, «убегает» ли переменная за пределы функции (например, мы вернули её адрес). Если да — переменная размещается в куче и живёт, пока на неё есть ссылки; сборщик мусора освободит её потом. Если нет — остаётся на стеке, что дёшево.
// newCounter возвращает адрес локальной переменной — и это БЕЗОПАСНО.
// Компилятор увидит, что n "убегает", и разместит её в куче.
func newCounter() *int {
n := 0
return &n
}
func main() {
p := newCounter()
*p++
fmt.Println(*p) // 1 — указатель полностью валиден
}
Практический вывод: в Go можно смело возвращать указатели на локальные переменные — язык гарантирует их корректность. Тебе не нужно вручную думать про стек и кучу; но полезно понимать, что «лишние» указатели могут увеличивать нагрузку на сборщик мусора, поэтому для мелких значений (int, небольшая структура) передача по значению часто и проще, и быстрее.
Создание указателей через new
Встроенная функция new(T) выделяет память под значение типа
T, инициализирует его нулевым значением и возвращает указатель
*T. На практике для структур чаще пишут &T{...},
но new полезен для базовых типов, когда нужен именно указатель.
p := new(int) // p имеет тип *int, *p == 0
*p = 7
fmt.Println(*p) // 7
Когда указатель НЕ нужен
Указатели — мощный инструмент, но не бесплатный: они усложняют рассуждение
о коде (кто и когда меняет объект?) и нагружают сборщик мусора. Не надо ставить
* везде «на всякий случай». Хорошие ориентиры: используй указатель,
если функция должна изменить аргумент, если структура большая
и копировать её дорого, или если тебе нужно значение nil как
маркер «нет данных». В остальных случаях — особенно для мелких значений вроде
int, bool, небольших структур из пары полей —
предпочитай передачу по значению: код проще и часто быстрее.
Отдельно отметим: слайсы, карты и каналы уже содержат внутри себя указатель
на общие данные, поэтому передавать *[]int или *map[K]V
почти никогда не нужно — функция и так сможет изменить элементы. Об этой
«указательной природе» слайсов подробно поговорим в следующей главе.
Сравнение указателей
Указатели можно сравнивать оператором ==. Два указателя равны,
если они показывают на одну и ту же ячейку памяти (или оба
nil). Это важно отличать от равенства значений: две разные
переменные могут хранить одинаковое число, но их адреса будут разными.
Сравнение указателей отвечает на вопрос «это буквально один и тот же объект?»,
а не «значения совпадают?».
a := 5
b := 5
p1 := &a
p2 := &a
p3 := &b
fmt.Println(p1 == p2) // true — оба показывают на a
fmt.Println(p1 == p3) // false — a и b это разные ячейки
fmt.Println(*p1 == *p3) // true — но значения одинаковы
На этой идее строится, например, проверка «одинаковый ли это узел» при обходе графов и связных списков: сравнивать сами узлы по значению может быть дорого или некорректно, а сравнение указателей — мгновенное и однозначное.
Указатель на поле структуры
Брать адрес можно не только у целой переменной, но и у поля структуры или элемента слайса. Это позволяет передать «ссылку» на конкретную часть большого объекта и менять именно её. Приём встречается, когда нужно точечно модифицировать вложенные данные без копирования всего контейнера.
type Point struct{ X, Y int }
func moveRight(x *int) {
*x += 10
}
func main() {
p := Point{X: 1, Y: 2}
moveRight(&p.X) // адрес конкретного поля
fmt.Println(p) // {11 2}
}
nil-указатель
Нулевое значение любого указателя — nil: «указывает в никуда».
Разыменование nil-указателя роняет программу паникой
nil pointer dereference. Поэтому перед разыменованием указателя,
который мог не быть проинициализирован, его проверяют на nil.
Здесь снова выручает короткое замыкание оператора &&:
проверку p != nil ставят слева, и если она ложна, правая часть не
выполняется.
package main
import "fmt"
func main() {
var p *int // nil
fmt.Println(p) // <nil>
// Безопасно: если p == nil, *p не вычисляется.
if p != nil && *p > 0 {
fmt.Println("положительное")
} else {
fmt.Println("указатель пуст или не положителен")
}
}
Кейс из реального проекта
Рассмотрим кэш конфигурации в сервисе. Мы хотим лениво загрузить настройки
один раз и потом переиспользовать. Указатель на структуру позволяет отличить
«ещё не загружено» (nil) от «загружено», а функция обновления
меняет объект по адресу, не копируя его. Плюс метод-геттер безопасно работает
с возможным nil.
package main
import "fmt"
type Config struct {
Host string
Port int
Debug bool
}
type ConfigCache struct {
current *Config // nil, пока не загрузили
}
// Load загружает конфиг в кэш, изменяя объект по адресу.
func (c *ConfigCache) Load(host string, port int) {
c.current = &Config{Host: host, Port: port, Debug: false}
}
// Addr безопасно строит адрес, учитывая, что конфиг мог не загрузиться.
func (c *ConfigCache) Addr() string {
if c.current == nil {
return "config not loaded"
}
return fmt.Sprintf("%s:%d", c.current.Host, c.current.Port)
}
// EnableDebug включает отладку в уже загруженном конфиге через указатель.
func (c *ConfigCache) EnableDebug() {
if c.current == nil {
return
}
c.current.Debug = true // меняем оригинал, а не копию
}
func main() {
var cache ConfigCache
fmt.Println(cache.Addr()) // config not loaded
cache.Load("api.internal", 8080)
fmt.Println(cache.Addr()) // api.internal:8080
cache.EnableDebug()
fmt.Println(cache.current.Debug) // true
}
Здесь указатель играет сразу три роли: маркер «загружено / не загружено»
(nil), способ дёшево делить один объект между методами и механизм
изменения этого объекта на месте. Всё это — типовой паттерн в бэкенде.
Типичные ошибки
Разыменование nil-указателя
Как НЕ надо: обращаться к *p или p.Field, не
убедившись, что p != nil. Функция могла вернуть
nil-указатель при ошибке, а ты сразу читаешь поле — и получаешь
панику в рантайме. Всегда проверяй указатели, которые пришли извне и могли быть
не заданы.
// Плохо: если find вернул nil, следующая строка паникует.
u := find(id)
fmt.Println(u.Name)
// Хорошо: сначала проверка.
u := find(id)
if u == nil {
fmt.Println("не найден")
return
}
fmt.Println(u.Name)
Ожидание изменений при передаче по значению
Как НЕ надо: передавать структуру в функцию по значению и надеяться, что
её изменения увидит вызывающий код. Функция работает с копией — оригинал
останется прежним. Если нужно изменить объект, передавай *T,
а не T.
Указатель на переменную цикла
Как НЕ надо (до Go 1.22): сохранять &item из цикла
range в слайс. Переменная цикла переиспользовалась, поэтому все
сохранённые указатели показывали на одну ячейку с последним значением. В
Go 1.22+ переменная своя на каждой итерации, но если пишешь код для более
старых версий — копируй элемент в локальную переменную перед взятием адреса.
// Надёжно везде: берём адрес локальной копии.
var ptrs []*Item
for _, it := range items {
it := it // локальная копия
ptrs = append(ptrs, &it)
}
Практика
- Напиши функцию
swap(a, b *int), которая меняет местами значения двух переменных через указатели. Проверь на паре чисел. - Реализуй
increment(counter *int), увеличивающую значение по адресу на единицу. Вызови её несколько раз в цикле. - Сделай структуру
Rectangle{Width, Height int}и функциюscale(r *Rectangle, factor int), умножающую обе стороны на множитель. Убедись, что оригинал меняется. - Напиши функцию
safeValue(p *int) int, которая возвращает*p, еслиp != nil, и ноль в противном случае. - Экспериментально: выведи адрес одной и той же переменной внутри функции
и в
main, передав её сначала по значению, потом по указателю. Сравни адреса и объясни разницу.
Итог
Указатели снимают ограничение «всё по значению»: храня адрес, мы можем менять
оригинал и дёшево передавать большие данные. Мы разобрали оператор взятия адреса
&, тип *T, разыменование *p для чтения
и записи, разницу между передачей по значению и через указатель, роль
указателей для изменяемости и производительности, функцию new и
опасность nil. Главное правило безопасности — проверять указатель
на nil перед разыменованием, и здесь помогает короткое замыкание
&&. Указатели лежат в основе методов, интерфейсов и почти
всех структур данных, которые мы встретим дальше. В следующей главе — массивы,
слайсы и карты, где указательная природа слайсов сыграет важную роль.