Получатель-указатель
В прошлом уроке метод был действием, которое умеет выполнять анкета. Но с какой анкетой работает метод — с вашим оригиналом или с ксерокопией? От ответа зависит, увидите ли вы его изменения. Метод AddPoints должен прибавлять очки игроку. Программа компилируется, но счёт после вызова остаётся нулевым:
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 } возвращает уже увеличенный счёт. Точка с запятой разделяет две инструкции в одной строке. Посмотрим на два вызова подряд:
- Первый вызов
pl.Add(5)получает копию соscore, равным0, и возвращает5. - Второй вызов получает новую копию
pl, гдеscoreпо-прежнему0, и тоже возвращает5.
Такой получатель подходит методам, которые только читают поля. Ловушка: запись в поле компилируется, но молча теряется.
Получатель-указатель
Чтобы метод менял саму переменную, получатель объявляют указателем: (p *Player). Это получатель-указатель. Звёздочка перед Player и есть всё отличие в записи. Вспомните бумажку с адресом дома из темы об указателях: такой метод получает не ксерокопию анкеты, а адрес оригинала.
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
}Давайте разберём пример по строкам:
var pl Playerсоздаёт игрока со счётом0.pl.AddPoints(5)идёт по адресуplи прибавляет5к самому полюscore.pl.AddPoints(3)прибавляет ещё3к тому же полю.fmt.Println(pl.score)печатает8.
Правило о копиях действует и здесь: метод получает копию бумажки с адресом, а копия ведёт к той же анкете.
Сравним оба получателя в одной программе. Нажмите «Следующий шаг» и следите за pl в рамке main: метод AddCopy меняет только свою копию.
1–4package main · import "fmt"5type Player struct{ score int }6 7func (p Player) AddCopy(k int) {8 p.score += k9}10 11func (p *Player) AddPoints(k int) {12 p.score += k13}14 15func main() {16 var pl Player17 pl.AddCopy(5)18 fmt.Println(pl.score)19 pl.AddPoints(5)20 pl.AddPoints(3)21 fmt.Println(pl.score)22}
var pl Player создаёт игрока со счётом 0.
Вызывать такой метод удобно:
- писать
(&pl).AddPoints(5)не нужно:pl— переменная, и Go сам передаёт её адрес; - если в переменной уже лежит указатель, например
ptr := &pl, вызовptr.AddPoints(5)тоже работает; - внутри метода
p.scoreобращается к полю через указатель, какr.Xу структуры; - через получатель-указатель метод и читает поля:
p.scoreв выражении даёт текущее значениеpl.score.
Ограничение у такого получателя одно: Go должен суметь взять адрес. Раздел о частой ошибке показывает, что бывает, когда Go не может.
Какой получатель выбрать
На собеседовании часто спрашивают, когда нужен получатель-указатель. Обычно отвечают так:
- Метод меняет поля — получатель-указатель.
- Метод только читает поля небольшой структуры — получатель-значение: изменить оригинал он всё равно не может.
- Структура большая — получатель-указатель даже для чтения: адрес копируется быстрее, чем все поля.
- Если хотя бы одному методу типа нужен указатель, руководство Go советует дать указатель всем его методам.
Последнее правило — совет, а не требование. Язык разрешает смешивать получатели, и такой код работает. Но когда все методы типа работают с оригиналом, читателю не нужно сверять каждый получатель.
Частая ошибка
Метод с получателем-указателем вызывают у литерала, а не у переменной:
Player{}.AddPoints(5) // ✗cannot call pointer method AddPoints on PlayerПрограмма не компилируется: Player{} — не переменная, и Go не может сам взять её адрес для метода. Сначала сохраните значение в переменную — pl := Player{} — и вызовите pl.AddPoints(5).
Главное
- Получатель-значение
(p Player)получает копию: изменения полей пропадают после вызова. - Получатель-указатель
(p *Player)меняет саму переменную; Go сам берёт её адрес при вызовеpl.AddPoints(5). - Метод, который меняет поля, объявляют с получателем-указателем.
- Метод с получателем-указателем нельзя вызвать у литерала вроде
Player{}.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.