Go: с нуля до своего блога Урок 12 из 50
Указатели и методы в Go: почему правка не сохранялась
Двенадцатый урок курса по Go. Ответ на ловушку прошлого урока: указатель — это адрес значения, а не его копия. Как работают & и *, чем метод отличается от функции, когда приёмник берут указателем, а когда значением, и почему for i := range чинит то, что for _, a := range ломает.
Зачем это нужно
В прошлом уроке правка внутри range пропала бесследно, и компилятор промолчал. Мы тогда сказали: настоящее решение — указатели. Вот оно.
Указатель нужен ровно там, где надо изменить чужое значение, а не его копию: опубликовать статью, дописать слов, поправить заголовок.
Сразу целиком
Новая папка, go mod init sabaq11, main.go:
package main
import "fmt"
type Article struct {
Title string
Words int
Published bool
}
func (a Article) ReadingTime() int {
return (a.Words + 199) / 200
}
func (a *Article) Publish() {
a.Published = true
}
func rename(a *Article, title string) {
a.Title = title
}
func main() {
first := Article{Title: "О степи", Words: 400}
fmt.Println(first.Title, "·", first.ReadingTime(), "мин ·", first.Published)
first.Publish()
rename(&first, "О степи: полная версия")
fmt.Println(first.Title, "·", first.Published)
blog := []Article{
{Title: "Привет, мир", Words: 150},
{Title: "Что такое шанырак", Words: 1000},
}
for i := range blog {
blog[i].Publish()
}
for _, a := range blog {
fmt.Println(a.Title, "·", a.Published)
}
p := &first
fmt.Printf("%T · %s\n", p, p.Title)
}
Прежде чем запускать — не заглядывая ниже, скажите вслух, что она напечатает. Потом запустите и сверьте.
О степи · 2 мин · false
О степи: полная версия · true
Привет, мир · true
Что такое шанырак · true
*main.Article · О степи: полная версия
Разбор
Указатель — это адрес, а не сама вещь
p := &first // взять адрес
fmt.Println(*p) // сходить по адресу и взять значение
& перед значением даёт его адрес. * перед указателем идёт по адресу обратно к значению. И то же * в объявлении типа означает «адрес такого-то»: *Article — «адрес статьи», а не статья.
%T это подтвердил: тип у p — *main.Article, со звёздочкой.
Образ. Ключ и фотография квартиры. Дали фотографию — рисуйте на ней что хотите, в квартире ничего не сдвинется. Дали ключ — можно прийти и переставить мебель по-настоящему. Указатель — это ключ.
Копия дешевле и безопаснее, поэтому Go копирует по умолчанию. Ключ выдают, когда надо менять.
Функция с указателем меняет оригинал
func rename(a *Article, title string) {
a.Title = title
}
rename(&first, "О степи: полная версия")
Параметр объявлен как *Article, и при вызове мы передаём &first — адрес. Заголовок поменялся снаружи, что мы и видим во второй строке вывода.
Сравните с прошлым уроком: там функция принимала Article, получала копию, и её правки никуда не доходили.
Метод — функция, приписанная к типу
func (a Article) ReadingTime() int {
Скобки перед именем — это приёмник. Они говорят: функция принадлежит типу Article, и вызывать её надо через точку — first.ReadingTime(), а не ReadingTime(first).
Образ. Инструкция, напечатанная на самой вещи. У стиральной машины режимы написаны на панели, а не в отдельной книге, которую ещё надо найти. Метод — то же самое: умение лежит на типе.
Приёмник: значение или указатель
Всё правило урока помещается в две строки.
func (a Article) ReadingTime() int // только читает — берите значение
func (a *Article) Publish() // меняет — берите указатель
ReadingTime считает и ничего не трогает: копии достаточно. Publish записывает в поле, и без указателя запись ушла бы в копию, а Published остался бы false — ровно та ловушка, с которой начался урок.
Одно уточнение на будущее: если хоть один метод типа берёт указатель, остальным обычно дают указатель тоже. Смешивать можно, но читателю вашего кода придётся каждый раз вспоминать, какой именно метод перед ним.
Go сам расставляет звёздочки
first — обычное значение, а не указатель. Тем не менее first.Publish() работает. И наоборот: p — указатель, а p.Title пишется без всякой звёздочки.
Go подставляет & и * сам, когда это однозначно. Поэтому в повседневном коде звёздочки почти не встречаются — только в объявлениях типов.
Как чинится ловушка прошлого урока
for i := range blog {
blog[i].Publish()
}
Здесь range даёт только номер. blog[i] — сам элемент среза, а не его копия, и Go берёт его адрес автоматически, потому что элемент среза адресуемый.
А for _, a := range blog { a.Publish() } по-прежнему не сделает ничего: a — копия, у копии есть свой адрес, и опубликуется именно она. Компилятор и здесь промолчит.
Запомнить просто: меняете — идите по индексу.
nil-указатель
Указатель может не указывать никуда. Его нулевое значение — nil:
var p *Article
fmt.Println(p.Title) // panic: invalid memory address or nil pointer dereference
Образ. Адрес на бумажке, по которому ничего не построено. Приехали — пустырь.
Это не редкость и не катастрофа: сообщение говорит прямо, что произошло. В уроке про ошибки мы научимся возвращать такие ситуации по-человечески.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Чем
func (a Article) ReadingTime()отличается отfunc (a *Article) Publish()и как выбрать между ними? first— не указатель, ноfirst.Publish()работает. Почему?for _, a := range blog { a.Publish() }не публикует ничего, аfor i := range blog { blog[i].Publish() }публикует. В чём разница?
Задание
Обязательное. Добавьте типу Article метод Shorten(limit int), который обрезает заголовок до limit букв и ставит в конце многоточие. Буквы, а не байты, — как в уроке про руны. Примените его ко всем статьям среза и напечатайте результат. Подумайте, каким должен быть приёмник, прежде чем писать.
По желанию.
- Вызовите
Shortenв циклеfor _, a := range blogи убедитесь, что ничего не изменилось. - Напечатайте
%Tдляfirstи для&first. - Напишите функцию
publishAll(blog []Article)и проверьте, работает ли она. Если нет — почему, и как это исправить одним символом в объявлении.
Куда это встанет в блоге
Дальше почти всё будет с указателями: обработчик получает *http.Request — и вы уже видели эту звёздочку в самом первом сервере, — база отдаёт *sql.Rows, а шаблон принимает *template.Template. Теперь понятно, что она там означает: вам дают ключ, а не фотографию.
Ответы
Показать ответы
- Первый только читает поля и получает копию — этого достаточно. Второй записывает в поле, и без указателя запись ушла бы в копию, которая тут же исчезнет. Правило: меняет — указатель, только читает — значение.
- Потому что Go сам подставляет
&, когда значение адресуемо.first— переменная, у неё есть адрес, и вызов превращается в(&first).Publish(). Писать звёздочки и амперсанды вручную приходится редко. - В первом случае
a— копия элемента; метод публикует копию, а она исчезает на следующем витке. Во второмblog[i]— сам элемент среза, у него есть адрес, и метод с указателем меняет то, что лежит в срезе.
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.