Перейти к содержимому
Теория · Структуры и методы · урок 5 из 11

Получатель-указатель

В прошлом уроке метод был действием, которое умеет выполнять анкета. Но с какой анкетой работает метод — с вашим оригиналом или с ксерокопией? От ответа зависит, увидите ли вы его изменения. Метод AddPoints должен прибавлять очки игроку. Программа компилируется, но счёт после вызова остаётся нулевым:

GO
package main

import "fmt"

type Player struct{ score int }

func (p Player) AddPoints(k int) { p.score += k }

func main() {
    var pl Player
    pl.AddPoints(5)
    fmt.Println(pl.score) // 0
}

Причина — в получателе метода. Получатель бывает двух видов, и сейчас мы разберём каждый.

Два вида получателя

Получатель — такой же параметр, как остальные, а параметр в Go получает копию аргумента, даже если аргумент — целая структура. Вид получателя решает, что копирует Go: всю анкету или бумажку с её адресом.

Получатель-значение

Получатель вида (p Player) называют получателем-значением. Его запись состоит из имени p и типа Player, без звёздочки.

При каждом вызове метод получает новую ксерокопию анкеты pl, а запись p.score += k меняет только эту копию. Ошибки компиляции нет: метод вправе менять свой получатель, но изменение пропадает вместе с копией.

Внутри метода копия меняется по-настоящему. Метод func (p Player) Add(k int) int { p.score += k; return p.score } возвращает уже увеличенный счёт. Точка с запятой разделяет две инструкции в одной строке. Посмотрим на два вызова подряд:

  1. Первый вызов pl.Add(5) получает копию со score, равным 0, и возвращает 5.
  2. Второй вызов получает новую копию pl, где score по-прежнему 0, и тоже возвращает 5.

Такой получатель подходит методам, которые только читают поля. Ловушка: запись в поле компилируется, но молча теряется.

Получатель-указатель

Чтобы метод менял саму переменную, получатель объявляют указателем: (p *Player). Это получатель-указатель. Звёздочка перед Player и есть всё отличие в записи. Вспомните бумажку с адресом дома из темы об указателях: такой метод получает не ксерокопию анкеты, а адрес оригинала.

GO
package main

import "fmt"

type Player struct{ score int }

func (p *Player) AddPoints(k int) { p.score += k }

func main() {
    var pl Player
    pl.AddPoints(5)
    pl.AddPoints(3)
    fmt.Println(pl.score) // 8
}

Давайте разберём пример по строкам:

  1. var pl Player создаёт игрока со счётом 0.
  2. pl.AddPoints(5) идёт по адресу pl и прибавляет 5 к самому полю score.
  3. pl.AddPoints(3) прибавляет ещё 3 к тому же полю.
  4. fmt.Println(pl.score) печатает 8.

Правило о копиях действует и здесь: метод получает копию бумажки с адресом, а копия ведёт к той же анкете.

Сравним оба получателя в одной программе. Нажмите «Следующий шаг» и следите за pl в рамке main: метод AddCopy меняет только свою копию.

ВживуюПолучатель-значение и получатель-указательшаг 1 из 9 · рамок: 1

var pl Player создаёт игрока со счётом 0.

Следите за pl в рамке main

Вызывать такой метод удобно:

  • писать (&pl).AddPoints(5) не нужно: pl — переменная, и Go сам передаёт её адрес;
  • если в переменной уже лежит указатель, например ptr := &pl, вызов ptr.AddPoints(5) тоже работает;
  • внутри метода p.score обращается к полю через указатель, как r.X у структуры;
  • через получатель-указатель метод и читает поля: p.score в выражении даёт текущее значение pl.score.

Ограничение у такого получателя одно: Go должен суметь взять адрес. Раздел о частой ошибке показывает, что бывает, когда Go не может.

Какой получатель выбрать

На собеседовании часто спрашивают, когда нужен получатель-указатель. Обычно отвечают так:

  • Метод меняет поля — получатель-указатель.
  • Метод только читает поля небольшой структуры — получатель-значение: изменить оригинал он всё равно не может.
  • Структура большая — получатель-указатель даже для чтения: адрес копируется быстрее, чем все поля.
  • Если хотя бы одному методу типа нужен указатель, руководство Go советует дать указатель всем его методам.

Последнее правило — совет, а не требование. Язык разрешает смешивать получатели, и такой код работает. Но когда все методы типа работают с оригиналом, читателю не нужно сверять каждый получатель.

Частая ошибка

Метод с получателем-указателем вызывают у литерала, а не у переменной:

GO
Player{}.AddPoints(5) // ✗
TEXT
cannot call pointer method AddPoints on Player

Программа не компилируется: Player{} — не переменная, и Go не может сам взять её адрес для метода. Сначала сохраните значение в переменную — pl := Player{} — и вызовите pl.AddPoints(5).

Главное

  • Получатель-значение (p Player) получает копию: изменения полей пропадают после вызова.
  • Получатель-указатель (p *Player) меняет саму переменную; Go сам берёт её адрес при вызове pl.AddPoints(5).
  • Метод, который меняет поля, объявляют с получателем-указателем.
  • Метод с получателем-указателем нельзя вызвать у литерала вроде Player{}.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом