Содержание урока · 15 разделов
- Методы в Go: функции, живущие при типе
- Анатомия метода: приёмник вместо первого параметра
- Приёмник-значение: метод работает с копией
- Приёмник-указатель: метод правит оригинал
- Вызов метода: Go сам ставит амперсанд и звёздочку
- Методы на своих типах: type Celsius float64
- Цепочка методов: метод возвращает свой тип
- Сеттер и геттер: методы в наборе
- Имя метода против имени поля
- Когда какой приёмник: правило согласованности
- Методы и интерфейсы на горизонте
- Шпаргалка: методы
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Методы в Go: функции, живущие при типе#
До сих пор поведение ваших типов собиралось из обычных функций: в уроке о функциях вы писали levelUp(player), где игрок приезжал параметром, а в уроке о структурах — levelUp(p *Player), который правил счёт через указатель. Это работало, но читалось задом наперёд: действие шло первым, а вещь, над которой оно совершается, — вторым. Методы Go переворачивают запись: функция объявляется вместе с приёмником — особым параметром, который пишется в скобках между словом func и именем метода, — и после этого вызывается точкой на значении: pl.LevelUp(). Вместо «возьми игрока и подними уровень» — «игрок поднял уровень». Разница на первый взгляд косметическая, но именно она делает большие программы читаемыми: у каждого типа виден весь набор методов — всё, что он умеет, перечислено в одном месте рядом с объявлением.
Методы Go — не классы из других языков: здесь нет конструкторов, наследования и ключевых слов this или self; приёмник — обычный параметр со своим именем, чаще всего коротким и одинаковым у всех методов типа. Нет и перегрузки: один и тот же метод нельзя объявить дважды с разными наборами параметров. Взамен Go даёт ясную модель — метод это функция, приёмник это её первый аргумент, а точка при вызове — просто другой синтаксис для передачи этого аргумента.
В этом уроке: анатомия объявления и чем приёмник отличается от параметра, приёмник-значение, работающий с копией, и приёмник-указатель, правящий оригинал, — прямое продолжение истории про указатели из 22-го; вызов метода на переменной и на указателе, где язык сам расставляет амперсанд и звёздочку; методы на именованных типах — не только структуры; цепочки вызовов, сеттер и геттер, конфликт имени метода с полем и правило согласованности приёмников. В финале — мост к интерфейсам: следующий-следующий большой сюжет курса начинается ровно отсюда.
Песочница: первый метод
Запустите: type Rect struct объявляет тип с двумя целыми полями (групповая форма W, H int из урока о структурах). Дальше два метода: func (r Rect) Area() int — в скобках приёмник r типа Rect, дальше имя Area, параметры (их здесь нет), возвращаемый тип и тело. Вызов идёт точкой на значении: r.Area() печатает 50, r.Perimeter() — 30. Приёмник внутри тела используется как обычная переменная: r.W, r.H читаются через точку. Опыты. Первый: добавьте метод Half() int, возвращающий r.W / 2, и напечатайте r.Half() — 2. Второй: соберите r2 := Rect{W: 3, H: 3} и вызовите r2.Area() — 9: один и тот же метод работает на любом значении своего типа. Третий: попробуйте вызвать fmt.Println(r.Area) без скобок — печать покажет адрес функции вида 0x4c6000: имя метода без круглых скобок — это уже значение-функция со связанным приёмником, а не вызов.
Запустите код, чтобы увидеть вывод.
Анатомия метода: приёмник вместо первого параметра#
Разберём объявление по частям. func (r Rect) Area() int читается так: слово func, затем в скобках приёмник — имя r и тип Rect, — затем имя метода Area, затем обычный список параметров, затем возвращаемые значения и тело. Всё, что идёт после приёмника, — обычная функция из урока о функциях: параметры, результаты, тело, ранние возвраты. Приёмник отличается от параметра только местом в объявлении и особым правом: его тип становится тем типом, на значениях которого метод доступен точкой.
Связь с функциями прямая: Area(r Rect) int и func (r Rect) Area() int — это одна и та же механика передачи значения, просто в методе приёмник переехал в скобки перед именем. Компилятор даже не даст объявить в том же пакете функцию и метод с одинаковыми «именами-сигнатурами» — пространства имён у функций и методов разные, но путаницу язык предотвращает заранее. Отсюда же правило именования приёмника: короткое имя, одно и то же у всех методов типа — r для Rect, c для Counter. Ни this, ни self: в Go их нет, и принимать стоит сразу, иначе чужой код из пакетов вроде fmt будет читаться странно. Запрос приёмник метода golang выдают и англоязычные статьи, и русские конспекты — суть везде одна: receiver, первый параметр метода, который точка при вызове подставит автоматически.
Два технических следствия анатомии. Первое: методы объявляются только на уровне пакета — рядом с типом, а не внутри функций; локальный метод внутри main язык не соберёт. Второе: метод объявляется для типов, объявленных В ЭТОМ ЖЕ пакете. Для int или string метод не напишешь — базовые типы живут в чужом пакете; но ничто не мешает завести своё имя для базового типа и писать методы ему — об этом середина урока.
И о перегрузке: у типа может быть только один метод с данным именем. Нет варианта Area() и Area(prec int) одновременно — для разной точности заводят разные имена. Это осознанное решение: без перегрузки вызов метода всегда однозначен, а чтение кода не требует смотреть на типы аргументов, чтобы понять, какой из вариантов вызовется.
Приёмник-значение: метод работает с копией#
Приёмник без звёздочки — value receiver — получает КОПИЮ значения, на котором метод вызван. Механика та же, что у параметров-значений из урока о функциях: структура копируется целиком, все поля, на всю глубину вложенности; то же копирование при присваивании вы видели в 23-м на b := a. Поэтому внутри метода с приёмником-значением можно свободно читать и… править поля, но правка останется внутри копии и исчезнет вместе с ней при выходе из метода. Value receiver — идеальный дом для методов-вопросов: площадь прямоугольника, сумма координат, округлённая температура — всё, что вычисляет ответ и ничего не меняет.
Здесь важно разделить две вещи, которые в разговорной речи часто сливаются. Правка полей ВНУТРИ value-метода легальна — компилятор пропустит c.N += d, просто эффект будет локальным. А вот правка ОРИГИНАЛА через value-метод невозможна в принципе: оригинал методу не передаётся, передаётся копия. Если метод должен менять состояние — писать в поля, инкрементировать счётчик, обновлять имя — нужен приёмник-указатель, о нём следующий блок. Это та же развилка, что в 20-м, где функция bump(n) не меняла наружную переменную, а через указатель — меняла; приёмники повторяют историю параметров в миниатюре.
Цена вопроса тоже параметрическая: копия стоит времени и памяти, пропорционально размеру структуры. Для Rect из двух int копирование — ноль забот; для структуры с сотней полей или большим массивом внутри каждый value-вызов тащит всю структуру за собой. Отсюда практический совет, к которому вернёмся в блоке о согласованности: маленькие и неизменяемые типы живут с value-приёмниками happily, крупные — обычно с pointer.
И честная оговорка про песочницу: копию приёмника движки дают по-разному. Основной движок и настоящий Go копируют честно — правка value-приёмника не видна снаружи; резервный движок привязывает приёмник по ссылке, и правка протекает в оригинал. Поэтому ранимые примеры урока устроены так, что value-методы свои поля не правят вовсе, а классическую демонстрацию «правка не видна» вы найдёте в неранимом блоке с точными выводами всех трёх исполнителей.
Приёмник-указатель: метод правит оригинал#
Звёздочка в приёмнике — func (c Counter) Inc() — делает метод методом-модификатором: при вызове приёмнику передаётся АДРЕС значения, и через этот адрес метод правит оригинал. Тело работает через сахар из 22-го: c.N внутри метода с приёмником-указателем — это (c).N, разыменование подставлено автоматически. Всё, что вы умели с указателями на структуры — levelUp(&pl) из 23-го, обмен через адреса из урока 22, — здесь то же самое, только адрес берётся языком сам при каждом вызове метода.
Прямой мост: в 23-м функция levelUp(p Player) прибавляла к счёту через p.Score += 5, и правка была видна снаружи, потому что наружу уехал адрес. Тот же уровень Up, переписанный методом, выглядит как func (p Player) LevelUp() { p.Score += 5 } и вызывается pl.LevelUp() — без амперсанда в месте вызова. Сигнатура стала короче, вызов — естественнее, а механика не изменилась ни на бит: указатель — указатель.
Pointer receiver нужен в трёх случаях. Первый — модификация: метод пишет в поля приёмника (Inc, Add, SetName, Reset). Второй — размер: структура большая, и копировать её на каждый вызов расточительно — тогда и методы-вопросы объявляют на указателе ради экономии. Третий — согласованность: если хотя бы один метод типа требует указателя, остальные обычно тоже объявляют на указателе, чтобы набор методов типа был однородным; почему это важно — отдельный блок ближе к концу урока.
Обратная сторона та же, что у любых указателей: nil. Указатель-приёмник может быть nil, и обращение к полю внутри метода по nil-адресу — то самое падение из 22-го. Разница с обычной функцией минимальна, но она есть: метод на nil-приёмнике теоретически может работать, если не трогает поля (так устроены пустые реализации в стандартной библиотеке), — для базового уровня достаточно знать, что nil-приёмник это живой указатель со всеми его правами и опасностями.
Песочница: приёмник-указатель меняет оригинал
Запустите: оба метода объявлены с приёмником-указателем. Первый вызов c.Inc() стоит на ПЕРЕМЕННОЙ — амперсанда нет, но правка доходит до оригинала: язык сам вызвал (&c).Inc(). Затем p := &c и вызов через указатель p.Inc() — то же самое, приёмник уже готовый адрес. После c.Add(5) счётчик 3+1+1+5 = 10, первая печать 10; ещё один c.Inc() даёт 11. Опыты. Первый: замените c.Add(5) на c.Add(100) — печати 105 и 106: все три правки через указатель сложились в одном оригинале. Второй: напечатайте p.N после всех вызовов — 11: через указатель видно то же самое поле, оригинал один. Третий: добавьте Reset() { c.N = 0 } на указателе, вызовите после правок и напечатайте c.N — 0: модификатор обнулил единственный оригинал.
Запустите код, чтобы увидеть вывод.
Проверьте себя: какой приёмник нужен
У типа Account есть поле Balance int. Метод Withdraw должен вычитать сумму из баланса ОРИГИНАЛА: внутри тела b.Balance -= sum. Как объявить метод?
Вызов метода: Go сам ставит амперсанд и звёздочку#
Самая приятная часть механики: при вызове метода язык молча приводит приёмник к нужному виду. У переменной c типа Counter и метода с pointer-приёмником запись c.Inc() — это на самом деле (&c).Inc(): амперсанд поставлен компилятором, потому что c — адресуемая переменная. И наоборот: у указателя p и метода с value-приёмником запись p.Value() — это (*p).Value(): звёздочка-разыменование подставлена сама. Строгое чтение каждый раз можно развернуть вручную, как вы разворачивали p.Field из 22-го, — а можно просто помнить: вызов метода сглаживает разницу между значением и указателем с обеих сторон.
Это зеркало сахара для полей: p.Name и (*p).Name — синонимы, c.Inc() и (&c).Inc() — синонимы. Одна форма для всех случаев, никакого выбора между точкой и стрелкой. Но у автоматизма есть граница — адресуемость: амперсанд нельзя взять у невыделенного значения. Классический случай — элемент карты: m["a"].Inc() у метода с pointer-приёмником не скомпилируется по той же причине, по которой в 23-м не скомпилировалось m["a"].X = 99, — поля элементов карты не адресуемы. Элементы среза адресуемы — cs[0].Inc() легален. Литерал структуры Point{1, 2}.Sum() настоящий Go разрешает — он собирает значение во временную переменную и берёт адрес от неё, хотя резервный движок песочницы такую запись не парсит, поэтому в ранимых примерах вызов метода всегда стоит на переменной.
Отсюда рабочее правило чтения чужого кода: вид приёмника в объявлении говорит, меняет ли метод состояние; вид переменной в месте вызова для этого не важен — компилятор сам всё приведёт. Если вызов не собирается с сообщением про cannot call pointer method или cannot take the address — ищите неадресуемый приёмник: элемент карты, результат функции, возвращающей значение, или литерал в резервном движке.
Что выведет код: оба сахара сразу
Прочитайте код и выберите, что он выведет:
package main
import "fmt"
type Counter struct {
N int
}
func (c Counter) Value() int {
return c.N
}
func (c *Counter) Inc() {
c.N++
}
func main() {
c := Counter{N: 1}
c.Inc()
p := &c
p.Inc()
fmt.Println(p.Value(), c.Value())
}Методы на своих типах: type Celsius float64#
Приёмником может быть не только структура — любой тип, объявленный в вашем пакете. Ключевое слово type умеет создавать имя для базового типа: type Celsius float64 вводит новый тип Celsius, под капотом — float64, со всеми его операциями: сложение, сравнение, печать. И у этого нового типа можно объявлять методы, как у структуры. Именно так в стандартной библиотеке живут time.Duration (int64 под капотом со своими методами Second(), Minutes()) и десятки маленьких доменных типов. Приёмник в объявлении ничем не отличается: func (c Celsius) Round() int — приёмник-значение скалярного типа.
Что это даёт? Методы прикрепляют поведение к смыслу: Celsius(21.5).Round() читается как «округли температуру», а не «вызови функцию округления от числа». Поля у Celsius нет — есть само значение, и в теле метода приёмник используется как обычный float64: к нему применимы арифметика и сравнения. Печатается значение как лежащий в основе float64: fmt.Println(Celsius(21.5)) даст 21.5 — если у типа нет метода String() (о нём ближе к концу урока), fmt печатает сырое значение.
Границы: метод нельзя объявить для типа из чужого пакета — float64, int, time.Time остаются без ваших методов; тип обязан быть объявлен в вашем пакете. Нельзя и объявить метод на алиасе базового типа, созданном через type X = float64: алиас — это другое имя того же типа, а не новый тип, и он по-прежнему «живёт» в чужом пакете. Различие обычного type X float64 (новый тип с собственным набором методов) и алиаса type X = float64 (второе имя) стоит держать в голове: методы прикрепляются только к первым.
Для базового уровня запомните шаблон: маленький смысловой скаляр — своё имя через type + value-методы для вопросов. Такой тип копируется дёшево, не требует указателя и часто возвращается из методов-цепочек, о которых следующий ранимый пример.
Песочница: метод на type Celsius и цепочка
Запустите: t — значение именованного типа Celsius, и у него вызывается метод Round(): печать 22 — int(21.5 + 0.5). Печать самого t показывает 21.5: под капотом float64, без метода String() fmt печатает сырое значение. Дальше Num: методы Add и Mul с value-приёмниками возвращают НОВЫЕ значения Num, и вызовы цепляются точками: n.Add(2).Mul(3).V — сначала 1+2, потом 3*3, потом поле V — печать 9. Опыты. Первый: добавьте метод ToF() float64, возвращающий float64(c)*1.8 + 32, и напечатайте t.ToF() — 70.7. Второй: продолжите цепочку: n.Add(2).Mul(3).Add(1).V — 10: результат каждого звена — снова Num, к которому применим следующий метод. Третий: попробуйте t.Add(0.5).Round() — основной движок и настоящий Go напечатают 22, а резервный теряет имя типа после первого звена и откажется: скалярные цепочки в резерве не работают, поэтому в ранимых примерах цепочки строятся на структурах, как Num.
Запустите код, чтобы увидеть вывод.
Цепочка методов: метод возвращает свой тип#
Цепочка вызовов работает тогда, когда метод возвращает значение того же типа, на котором вызван: n.Add(2) — это Num, и у него можно вызвать следующий метод. Так строятся беглые интерфейсы — fluent API: конструкторы запросов, билдеры настроек, преобразования значений длинной лентой. Правило симметричное: если метод возвращает другой тип, цепочка на нём продолжится уже методами этого другого типа — n.Area().Print() сработает, если Area возвращает не Num, а что-то, у чего есть Print.
Два вида приёмников дают два характера цепочек. Value-методы, возвращающие новые значения, — чистые звенья: исходное значение не меняется, каждая ступень — отдельная копия, лента читается слева направо как конвейер. Это доминирующий стиль для маленьких типов вроде Num или строковых обёрток. Pointer-методы теоретически тоже могут возвращать приёмник — return c — и цепочка будет править один и тот же оригинал на каждом звене; это уже не конвейер, а последовательность мутаций, и в чужом коде её стоит читать внимательнее: результат последнего звена и промежуточные состояния — одна и та же переменная.
Практический нюанс песочницы, который вы только что видели в опытах: цепочки на именованных скалярных типах в резервном движке ломаются — после первого вызова значение теряет имя типа, и следующий метод не находится. Настоящий Go и основной движок считают такие цепочки без проблем. Поэтому правило урока простое: в ранимых примерах цепочки строятся на структурах (тип имени сохраняется в копии), а скалярные цепочки — материал для настоящего Go.
И вишня для читателей чужого кода: длинная цепочка из шести звеньев — это шесть копий маленькой структуры и ноль побочных эффектов, если все приёмники value. Если хотя бы одно звено с pointer-приёмником — вся лента живёт на одном адресе. Вид приёмников в объявлении типа — единственное, что нужно пробежать глазами, чтобы понять характер цепочки.
Сеттер и геттер: методы в наборе#
Соединим указательные и значимые приёмники в самый частый промышленный паттерн — сеттер и геттер. Сцена: поле структуры сделано непубличным — name со строчной буквы, как вы узнали про экспортируемость в 23-м, — и наружу выдаётся только через методы: SetName записывает, Name читает. Запись пойдёт через приёмник-указатель — правка оригинала; чтение — через приёмник-значение — копия дешевле и логики записи не требует. Так тип контролирует собственные инварианты: сеттер может проверять ввод, геттер — вычислять производное, а прямого доступа к полю снаружи нет.
Именование — знак отличия Go: геттер называется именем поля без Get — Name(), а не GetName(); сеттер — SetName(). Идиома закреплена в официальных советах по стилю, и код, полный GetName(), мгновенно выдаёт программиста, переехавшего с Java. Причина проста: p.Name() и p.Name читаются почти одинаково, лишний слог только шумит.
Фраза методы в наборе — это и про method set, набор методов типа: все методы, объявленные для типа, в одном списке — его «меню». Набор методов структур из 23-го был пуст (тип умел только хранить), теперь у каждого вашего типа меню пополняется каждой строкой func (r T) .... У набора методов есть формальная жизнь в правилах языка: методы с value-приёмником входят в набор и самого типа T, и его указателя T; методы с pointer-приёмником — только в набор T. Пока вы не работаете с интерфейсами, различие незаметно, но оно станет главным в уроке про интерфейсы: реализуются ли типом требования интерфейса — решает именно набор методов. Фундамент заложен здесь: пишете p.SetName(...) и p.Name() — заполняете набор, из которого интерфейсы потом черпают совпадения.
Пара слов о границах паттерна: для простых структур с публичными полями сет/гет часто избыточен — прямое поле короче и честнее; сеттеры берут, когда есть инварианты (неотрицательный баланс), валидация или желание сменить хранение позже, не ломая вызовы.
Песочница: сеттер и геттер
Запустите: поле name со строчной буквы — непубличное, но внутри одного пакета (а песочница — один пакет main) оно доступно, поэтому программа собирается. p.SetName("аня") — pointer-приёмник правит оригинал пустой структуры; p.Name() — value-приёмник читает копию, печатается «аня». Печать самой структуры fmt.Println(p) даёт {аня}: метод Name() при печати не вызывается — fmt показывает поля как есть. Опыты. Первый: переименуйте геттер в GetName() и обновите вызов — работает, но запомните: идиома Go — Name(). Второй: добавьте сеттер SetScore(score int) и поле score int — записи через указатель копятся. Третий: добавьте в SetName проверку if name == "" { return } и вызовите p.SetName("") — печать p.Name() покажет прежнее имя: сеттер отбил невалидный ввод, чего не умело прямое присваивание полю.
Запустите код, чтобы увидеть вывод.
Имя метода против имени поля#
Поле и метод живут в разных мирах — но с одним общим пространством имён: у типа НЕ МОЖЕТ быть поля и метода с одним именем. type Weird struct { Value int } и func (w Weird) Value() int в одном пакете не соберутся — настоящий Go откажется с сообщением field and method with the same name. Это защита от неоднозначности: чем должен стать w.Value в выражении — полем или вызовом? Язык не гадает, а запрещает конфликт на этапе компиляции.
Отсюда практика именования: метод-вопрос для поля Value называют как поле (геттер из прошлого блока — Name() для поля name), и тогда конфликты всплывают сразу — решением становится переименование поля или метода. В стандартной библиотеке вы встретите пары вроде bytes.Buffer: поле buf непубличное, а публичный метод Len() даёт длину — имена разведены намеренно. Если при чтении чужого кода видите метод, называющийся как будто это поле, — внутри почти наверняка непубличное поле с похожим, но другим именем.
Второе ограничение имени: методы не перегружаются — одно имя у типа занимает ровно один метод, повторное объявление с другими параметрами не соберётся. Зато одно имя может жить у разных типов: у Rect.Area() и Circle.Area() конфликтов нет — наборы методов у каждого свои.
И третье, о чём уже говорилось в анатомии, но стоит закрепить: методы объявляются только на уровне пакета. Внутри main объявить func (c Counter) Reset() нельзя — синтаксическая ошибка; локально можно объявить тип (вы делали это в fill-заданиях), но методы к нему — только снаружи, рядом с другими объявлениями пакета. Всё, что принадлежит типу, живёт в одном месте файла — и это не строгость ради строгости, а прямое следствие идеи «набор методов как меню типа»: меню должно быть просматриваемым целиком.
Что выведет код: копия приёмника
Прочитайте код и выберите, что он выведет:
package main
import "fmt"
type Counter struct {
N int
}
// Added возвращает новый счётчик, приёмник не трогает
func (c Counter) Added(d int) Counter {
return Counter{N: c.N + d}
}
func main() {
c := Counter{N: 5}
c2 := c.Added(10)
fmt.Println(c2.N, c.N)
}Классика: value-приёмник пытается менять
GOpackage main
import "fmt"
type Counter struct {
N int
}
// Broken пытается изменить приёмник-значение
func (c Counter) Broken() {
c.N = 99
}
func main() {
c := Counter{N: 5}
c.Broken()
fmt.Println(c.N)
}Кнопки нет: движки песочницы расходятся ровно на этом примере. Настоящий Go и основной движок печатают 5: приёмник-значение — копия, правка c.N = 99 исчезла вместе с копией при выходе из метода. Резервный движок привязывает value-приёмник по ссылке и протекает: печатает 99. Такая же утечка у него и в методе с возвратом: Add(d int) int { c.N += d; return c.N } даст «15, затем 5» у настоящего Go и yaegi, но «15, затем 15» у резерва. Поэтому в ранимых примерах урока value-методы свои поля не правят вовсе — вопросами занимаются чистые методы, а правкой — приёмники-указатели. В настоящем Go запомните одно: правка в value-приёмнике — всегда потерянная работа.
Когда какой приёмник: правило согласованности#
Соберём развилку в правило. Pointer receiver нужен, если метод правит приёмник, если структура велика и копирование дорого, если внутри поля каналы, мьютексы или другие вещи, которые копировать нельзя, и — по согласованности — если хотя бы один метод типа уже указательный. Value receiver достаточен для чистых методов-вопросов над маленькими типами: числа-обёртки вроде Celsius, точки, пары, неизменяемые значения. Сама стандартная библиотека миксует осознанно: time.Time целиком живёт на value-приёмниках — время неизменяемо, и каждый метод возвращает новое значение; а bytes.Buffer целиком на указателе — буфер мутирует по определению. Подсказка прямо в определении: если для понимания метода не важно, копия перед вами или оригинал, — скорее value; если важно — pointer.
Главный инженерный аргумент — согласованность: все методы одного типа должны иметь один вид приёмника. Смешанный набор методов типа работает, но создаёт ловушку на ровном месте: value-методы входят в набор и T, и T, а pointer-методы — только в T, и при переходе к интерфейсам (когда набор методов станет договором) окажется, что у T и *T разные возможности. Смешанный тип — это два разных набора в одной обёртке, и расследование, почему интерфейс то принимается, то нет, начинается ровно с этого. Выбирайте указатель по умолчанию для структур с состоянием, value — для маленьких неизменяемых, и не меняйте вид внутри одного типа.
Термины для чтения документации: в англоязычных текстах это value receiver и pointer receiver; оба термина — про первый параметр метода, а не про параметры в скобках. Русские конспекты говорят «приёмник-значение» и «приёмник-указатель». Встречаются и советы «всегда pointer» — разумный дефолт для больших структур, но слепое следование ломает как раз маленькие чистые типы вроде Celsius, где копия дешевле смысла указателя.
И мост к интерфейсам, который здесь уместен: интерфейс в Go — это набор методов, и тип реализует интерфейс, когда его набор методов покрывает требования. Всё, что вы наработали в этом уроке — вызов метода, оба вида приёмников, набор методов, — станет словарём следующего большого сюжета; до него — финальное приложение курса, где методы заработают в настоящей программе.
String(): метод, который знает fmt
GOpackage main
import "fmt"
type Celsius float64
// String форматирует температуру для печати
func (c Celsius) String() string {
return "21C"
}
func main() {
t := Celsius(21)
fmt.Println(t)
}Кнопки нет: расхождение движков. Настоящий Go и основной движок песочницы печатают «21C»: у fmt есть правило — если у значения есть метод String() string, он используется для печати (это интерфейс fmt.Stringer, о наборах методов вы уже читали). Резервный движок методы при печати не проверяет и печатает сырое значение: «21». Выполняя код в настоящем Go, помните: метод String() — самый дешёвый способ сделать печать своего типа человеческой; здесь он вернул константу для простоты, в реальном коде форматировал бы само значение приёмника.
Методы и интерфейсы на горизонте#
Подведём конструкцию под крышу. Вы умеете объявлять методы с обоими приёмниками, читать вызов метода на переменной и на указателе, строить цепочки, сеттеры и геттеры, избегать конфликта имени с полем и выбирать приёмник по задаче. Осталась последняя связка — зачем всё это языку: набор методов определяет, ЧТО умеет тип, а интерфейсы описывают, ЧТО от типа требуется. Совпали — тип можно передать туда, где ждут интерфейс, и методы заработают в чужом коде: fmt уже сделал это с вашим будущим String(). Так маленькие методы из этого урока превращаются в строительные блоки полиморфизма — без наследования и магии, просто совпадение имён в наборе.
Практический план на закрепление: перепишите свои старые функции на методы. levelUp(p *Player) из 23-го — LevelUp() на указателе; функции над картами структур — методы значения; маленькие доменные скаляры — типы с value-методами. Каждая переписанная функция — минус один параметр в голове и плюс одна строка в наборе методов типа.
Курс подбирается к финалу: следующий урок — 25-pervoe-prilozhenie — соберёт первое приложение, консольный менеджер заметок, и заметки там будут не сырыми структурами, а типами с методами: добавить, пометить сделанной, напечатать список. Всё, что вы разобрали на Counter и Celsius, переедет в настоящую программу как есть. А после приложения курс приоткроет интерфейсы — и наборы методов из этого урока станут тем, по чему Go решает, подходит ли тип. Оглавление курса — перед финалом самое время пробежаться по своим конспектам.
Шпаргалка: методы#
| Запись | Что делает | В песочнице |
|---|---|---|
| func (r Rect) Area() int | метод с value-приёмником: работает с копией | работает |
| func (c *Counter) Inc() | метод с pointer-приёмником: правит оригинал | работает |
| c.Inc() на переменной | Go сам берёт адрес: (&c).Inc() | работает |
| p.Value() на указателе | Go сам разыменовывает: (*p).Value() | работает |
| cs[0].Inc() | pointer-метод на элементе среза — элемент адресуем | работает |
| w.Inner.Inc() | метод на вложенном поле | работает |
| type Celsius float64 + метод | метод на именованном скалярном типе | работает |
| n.Add(2).Mul(3).V | цепочка value-методов на структуре | работает |
| p.SetName("аня") / p.Name() | сет/гет-паттерн над непубличным полем | работает |
| правка в value-приёмнике | настоящий Go: копия, оригинал цел; резерв протекает | неранимо |
| String() + fmt.Println(t) | yaegi печатает «21C»; резерв — сырые «21» | неранимо |
| Point{1, 2}.Sum() | вызов на литерале; резерв не парсит точку | неранимо |
| func (c *C) Add(n int, m int) | per-param в методе; резерв не парсит запятую | неранимо; групповая форма Add(n, m int) |
Вставьте пропуски: приёмники и вызов метода
Соберите программу: объявите приёмник-указатель, возьмите адрес и вызовите метод-модификатор. Собранная программа печатает «3 3».
Соберите программу: счётчик с методами
Соберите программу: тип Meter, два метода — value-метод-вопрос и pointer-метод-модификатор, — правка и печать. Собранный вывод — «20».
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Каждый ранимый пример урока прогнан пробником на обоих движках песочницы. Совпадают: парсинг приёмников обоих видов (r T) и (r T), pointer-мутация оригинала — и на переменной с автоматическим амперсандом, и через готовый указатель, вызов value-метода на указателе с автоматическим разыменованием, методы на именованном типе type Celsius float64 с печатью сырья, цепочки value-методов на структурах, сет/гет над непубличным полем, вызов pointer-метода на элементе среза и вложенном поле, метод value f := c.Value с последующим вызовом. Расходятся или недоступны, все случаи проверены пробником: (1) правка в value-приёмнике — настоящий Go и yaegi держат копию (Broken печатает 5, Add с возвратом — «15, затем 5»), резерв привязывает приёмник по ссылке и протекает (99; «15, затем 15») — блок неранимый; (2) String() при печати fmt — yaegi и настоящий Go печатают «21C», резерв — сырое «21» — неранимо; (3) вызов метода на литерале Point{1, 2}.Sum() — резерв не парсит («ожидалось „)“, получено „.“»), настоящий Go разрешает — в ранимых вызов на переменной; (4) per-param сигнатура в методе Add(n int, m int) — резерв не парсит («ожидался тип, получено „,“») — в ранимых групповая форма Add(n, m int); (5) цепочка методов на именованном скалярном типе t.Add(0.5).Round() — резерв теряет имя типа после первого звена («у значения типа float64 нет поля или метода Round») — в ранимых цепочки на структурах; (6) срез указателей с элизией типа []Counter{{N: 1}} — резерв не собирает («композитный литерал для этого типа не поддерживается») — в ранимых литералы с явным &Counter{...} либо без указателей.
Карточки: методы
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Метод — функция с приёмником: func (r Rect) Area() int; приёмник — первый параметр, живущий в скобках перед именем; вызывается точкой: r.Area().
- Value receiver работает с копией: правки полей внутри метода не выходят наружу; территория чистых методов-вопросов над маленькими типами.
- Pointer receiver получает адрес оригинала: func (c *Counter) Inc() правит через сахар (*c).N; нужен для модификации, больших структур и согласованности.
- Вызов метода приводит приёмник сам: c.Inc() — это (&c).Inc(), p.Value() — это (*p).Value(); граница — адресуемость (элемент карты не адресуем, элемент среза адресуем).
- Методы объявляются для типов своего пакета: type Celsius float64 + value-методы — идиома для маленьких смысловых скаляров; печать без String() показывает сырую основу.
- Цепочка методов работает, когда метод возвращает свой тип; value-цепочки — конвейер копий, pointer-звенья живут на одном адресе.
- Сет/гет-паттерн: непубличное поле + SetName на указателе + Name на значении; геттер без Get — идиома Go; сеттер добавляет инварианты и валидацию.
- У типа не может быть поля и метода с одним именем; методы не перегружаются; объявляются только на уровне пакета.
- Набор методов (method set): value-методы принадлежат T и *T, pointer-методы — только *T; держите приёмники одного типа согласованными — на наборе методов стоят интерфейсы.
Домашнее задание
Соберите библиотеку с методами. Шаг первый: type Book struct { title string; author string; read bool } — все поля непубличные. Шаг второй: геттер Title() string и Author() string на value-приёмниках, сеттер MarkRead() на указателе, который ставит read = true. Шаг третий: соберите книгу именованным литералом из 23-го, вызовите MarkRead() и напечатайте b.Title() и саму книгу fmt.Println(b) — в печати появится true. Шаг четвёртый: добавьте value-метод Describe() string, возвращающий строку ««Название» — Автор (прочитана)» — соберите её из геттеров и if b.read; вызовите в main. Шаг пятый: type Shelf struct { books []Book } с pointer-методом Add(b Book) и value-методом Count() int; добавьте две книги и напечатайте Count(). В комментариях запишите, почему MarkRead обязан быть на указателе, а Count — нет. [Оглавление курса](/courses/go-basics/) под рукой; в следующем уроке методы переедут в первое приложение — менеджер заметок.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем метод отличается от функции и когда писать метод, а когда функцию?
Механика одна: приёмник метода — это его первый параметр, переставленный в скобки перед именем. Разница в чтении и организации: метод принадлежит набору методов типа, вызывается точкой и виден в «меню» типа рядом с объявлением; функция живёт отдельно и требует передачи значения параметром. Правило большого пальца: если логика неотделима от данных типа — метод; утилита над чужими типами или чистое преобразование без владельца — функция. Методы нельзя объявить для типов чужих пакетов — для float64 и time.Time пишут функции.
Почему метод-модификатор обязан иметь приёмник-указатель?
Приёмник-значение получает копию структуры: присваивания полям внутри метода меняют копию и исчезают при выходе — оригинал остаётся прежним. Это та же семантика значений, что у параметров функций из урока о функциях и у присваивания b := a из урока о структурах. Приёмник-указатель передаёт адрес оригинала, и правка через (*c).N доходит до вызывающего кода. Дополнительно: большие структуры объявляют на указателе даже для методов-вопросов — чтобы не копировать данные на каждый вызов.
Почему c.Inc() работает, хотя Inc объявлен на указателе, — и когда такой вызов не соберётся?
Для адресуемых значений Go сам ставит амперсанд: c.Inc() — это (&c).Inc(); а p.Value() для value-метода на указателе — это (*p).Value(). Граница — адресуемость: амперсанд нельзя взять у элемента карты (m["a"].Inc() не скомпилируется — та же причина, по которой не работает m["a"].X = 99), у результата функции, возвращающей значение, и у значения в канале. Элементы среза адресуемы — cs[0].Inc() легален. Сообщение компилятора про cannot take the address — сигнал искать неадресуемый приёмник.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Продвинутые возможности Go: от замыканий до рефлексии
Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Писать и читать конкурентный код: go-функции, каналы, select
- Управлять жизненным циклом операций через context
- Проектировать API на интерфейсах
- Использовать дженерики без злоупотребления