Указатели в Go: основы, функции, структуры
В уроке 02-memory-and-parameters ты узнал, что в Go всё передаётся по значению, и единственный способ, чтобы функция изменила что-то снаружи, это передать указатель. Здесь разложим, как с ними работать на практике, какие операции есть, и где лежат ловушки.
В Go нет арифметики указателей (как в C), нет const-указателей (как в
C++), нет умных указателей и циклических ссылок (как ARC в Swift).
Просто пара операторов и одно правило: указатель может быть nil,
проверяй.
1. & и *
Два главных оператора:
&x, “адрес переменной x”. Возвращает указатель наx.*p, “то, на что указывает p”. Возвращает значение по адресу (или присваивает по нему:*p = 5).
x := 10
p := &x // p имеет тип *int
fmt.Println(p) // 0xc0000180a0 -- адрес
fmt.Println(*p) // 10 -- значение
*p = 99
fmt.Println(x) // 99 -- поменяли x через указатель
Тип указателя записывается со звёздочкой слева: *int это “указатель
на int”, *Config это “указатель на Config”.
Указатель указателя
Можно. Используют редко (двойная косвенность), но легально:
y := 1
p := &y // *int
pp := &p // **int
**pp = 99
fmt.Println(y) // 99
В Go-коде на собесе это иногда показывают для тренировки внимательности. В проде ты вряд ли встретишь.
2. Указатель и функция
Изменение значения
Без указателя функция не может изменить переменную снаружи:
func IncByValue(n int) {
n++ // меняет копию
}
func IncByPointer(n *int) {
*n++ // меняет то, на что указывает
}
x := 5
IncByValue(x)
fmt.Println(x) // 5
IncByPointer(&x)
fmt.Println(x) // 6
Это важно для функций swap, sort, и любых “in-place” операций:
func Swap(a, b *int) {
*a, *b = *b, *a
}
x, y := 1, 2
Swap(&x, &y)
fmt.Println(x, y) // 2 1
Возврат указателя
Функция может вернуть указатель на локальную переменную, и это безопасно. Компилятор увидит escape и положит её в heap (см. урок 02 §4):
func NewBuffer(size int) *Buffer {
b := Buffer{cap: size, data: make([]byte, 0, size)}
return &b // OK, b "ускользает", уйдёт в heap
}
Это противоположность C, где возврат адреса локалки даёт dangling pointer и UB. В Go рантайм просто аллоцирует в куче, GC потом приберёт.
3. Указатели и структуры
Главный вопрос: когда писать Foo в типе, а когда *Foo. Шпаргалка:
| Цель | Тип |
|---|---|
| “это immutable copy, маленькая” | Foo |
| “это shared mutable state” | *Foo |
| “это большой объект, копировать дорого” | *Foo |
| “может быть отсутствие (optional)” | *Foo (или sql.Null…) |
Точка как сахар
В отличие от C, где p->field это поле через указатель, в Go обе
формы используют точку, и компилятор сам понимает:
type User struct {
Name string
}
u := User{Name: "Vasya"}
p := &u
fmt.Println(u.Name) // "Vasya"
fmt.Println(p.Name) // "Vasya", автоматическое (*p).Name
fmt.Println((*p).Name) // тоже OK, но никто так не пишет
То же с методами на pointer receiver: можно вызывать на значении, если оно addressable:
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
c := Counter{}
c.Inc() // OK, Go сам сделает (&c).Inc()
fmt.Println(c.n) // 1
m := map[string]Counter{"a": {}}
m["a"].Inc() // ошибка компиляции: m["a"] не addressable
С мапами всегда так: значения мапы не addressable, иначе мутация была
бы неожиданной (изменили бы скрытую копию). Решение, либо хранить
map[string]*Counter, либо явно вытаскивать, мутировать, класть назад.
4. nil pointer и его ловушки
Указатель может быть nil, “никуда не указывает”:
var p *int // nil
fmt.Println(p) // <nil>
fmt.Println(*p) // panic: runtime error: invalid memory address
Это самая частая паника в Go. Идиоматичная защита, проверка:
func SafeGet(c *Config) int {
if c == nil {
return 0
}
return c.Timeout
}
Когда ожидать nil:
- Параметр функции, который “опционален”. Соглашение, документировать в комментарии.
- Значение из мапы, через
m[k], если значение, это*Foo. - Возврат из функции, в паре с
error. Идиома:if err != nil { return nil, err }.
nil-pointer ресивер
Удивительно, но в Go можно вызвать метод на nil-указателе, если
метод этого ожидает:
type List struct {
head *Node
}
type Node struct {
val int
next *Node
}
func (n *Node) Len() int {
if n == nil {
return 0
}
return 1 + n.next.Len()
}
var n *Node
fmt.Println(n.Len()) // 0, не паника
Это удобно для линкед-листов, деревьев и прочих структур, где nil,
это “пустая ветка”. Но сначала обязательно проверка n == nil в начале
метода.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.