Раздел 22 · Go

Указатели в Go: основы, функции, структуры

junior~55 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Перед этим уроком

Указатели в 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-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.