---
title: "Методы структур"
description: "Методы go: приёмник-значение и приёмник-указатель, вызов метода на переменной и указателе, методы на type Celsius, сеттер и геттер, выбор приёмника."
course: "Go с нуля: базовый синтаксис и конструкции"
url: "https://golangmaster.ru/courses/go-basics/lessons/24-metody/"
published: "2026-09-19"
updated: "2026-09-28"
minutes: 22
---

# Методы структур

*Курс «Go с нуля: базовый синтаксис и конструкции», урок 24 из 25. ~22 минут. Обновлено 28.09.2026. Источник: https://golangmaster.ru/courses/go-basics/lessons/24-metody/*

Методы Go — это функции, у которых есть приёмник: особый первый параметр, прикреплённый к типу записью func (r Receiver) Name(...). В [уроке о структурах](/courses/go-basics/lessons/23-struktury/) тип Player умел только хранить поля, а счёт поднимала внешняя функция levelUp — в этом уроке levelUp станет методом: levelUp() вызывается точкой на самой структуре, и код читается как действие над вещью, а не как процедура с параметром. Разберём оба вида приёмника — value receiver, работающий с копией, и pointer receiver, правящий оригинал (прямое продолжение [урока об указателях](/courses/go-basics/lessons/22-ukazateli/)), — вызов метода на переменной и на указателе, где Go сам расставляет амперсанд и звёздочку, методы на именованных типах вроде type Celsius float64, цепочки вызовов и сет/гет-паттерн. Каждый ранимый пример прогнан на обоих движках песочницы, а расхождения честно оговорены.

### Методы в Go: функции, живущие при типе

До сих пор поведение ваших типов собиралось из обычных функций: в уроке о функциях вы писали levelUp(player), где игрок приезжал параметром, а в [уроке о структурах](/courses/go-basics/lessons/23-struktury/) — 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: имя метода без круглых скобок — это уже значение-функция со связанным приёмником, а не вызов.

```go
package main

import "fmt"

// Rect — прямоугольник: ширина и высота
type Rect struct {
        W, H int
}

// Area возвращает площадь — приёмник-значение
func (r Rect) Area() int {
        return r.W * r.H
}

// Perimeter возвращает периметр
func (r Rect) Perimeter() int {
        return 2 * (r.W + r.H)
}

func main() {
        r := Rect{W: 5, H: 10}
        fmt.Println(r.Area())
        fmt.Println(r.Perimeter())
}
```

### Анатомия метода: приёмник вместо первого параметра

Разберём объявление по частям. 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](/courses/go-basics/lessons/22-ukazateli/), — здесь то же самое, только адрес берётся языком сам при каждом вызове метода.

Прямой мост: в 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: модификатор обнулил единственный оригинал.

```go
package main

import "fmt"

// Counter — счётчик с целым значением
type Counter struct {
        N int
}

// Inc увеличивает счётчик на единицу — приёмник-указатель
func (c *Counter) Inc() {
        c.N++
}

// Add прибавляет d — тоже приёмник-указатель
func (c *Counter) Add(d int) {
        c.N += d
}

func main() {
        c := Counter{N: 3}
        c.Inc()
        p := &c
        p.Inc()
        c.Add(5)
        fmt.Println(c.N)
        c.Inc()
        fmt.Println(c.N)
}
```

### Проверьте себя: какой приёмник нужен

**Вопрос:** У типа Account есть поле Balance int. Метод Withdraw должен вычитать сумму из баланса ОРИГИНАЛА: внутри тела b.Balance -= sum. Как объявить метод?

- ✅ func (b *Account) Withdraw(sum int): правка оригинала требует приёмник-указатель — value-приёмник работает с копией и правка исчезнет
- func (b Account) Withdraw(sum int): value-приёмник короче, а править поля он умеет так же
- func Withdraw(b Account, sum int): методы не умеют менять поля, нужна обычная функция с указателем
- func (b *Account) Withdraw(*int): раз нужна правка, указатель должен быть и в приёмнике, и в параметре

Метод-модификатор объявляется с приёмником-указателем: только он получает адрес оригинала. Приёмник-значение получил бы копию Account, вычитание изменило бы копию, и после выхода из метода баланс остался бы прежним. Обычная функция с указателем тоже сработала бы — но методы для того и придуманы, чтобы вызов читался acc.Withdraw(50). Параметр sum остаётся обычным int: указатель нужен там, где нужен адрес, а не везде подряд.

### Вызов метода: 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 — ищите неадресуемый приёмник: элемент карты, результат функции, возвращающей значение, или литерал в резервном движке.

### Что выведет код: оба сахара сразу

```go
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())
}
```

**Что выведет программа?**

- ✅ 3 3: Go сам ставит амперсанд для p.Inc() и сам разыменовывает p.Value() — обе печати смотрят на один счётчик
- 2 3: вызов p.Value() читает копию, сделанную до второго Inc
- 3 2: правка через указатель p не видна в переменной c
- Программа не соберётся: value-метод нельзя вызывать на указателе

c.Inc() правит оригинал — счётчик 2. p.Inc() — приёмник уже указатель, правит тот же оригинал — счётчик 3. Затем p.Value(): метод с value-приёмником, вызывается на указателе — язык разыменовывает сам, (*p).Value(), и печатает 3. c.Value() печатает те же 3: p и c смотрят на одну переменную. Оба сахара работают одновременно и независимо — пробник прогнал код на обоих движках песочницы, вывод одинаков: «3 3».

### Методы на своих типах: 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.

```go
package main

import "fmt"

// Celsius — температура на базе float64
type Celsius float64

// Round округляет до целых градусов
func (c Celsius) Round() int {
        return int(c + 0.5)
}

// Num — число с методами-цепочками
type Num struct {
        V int
}

// Add возвращает новое число
func (n Num) Add(x int) Num {
        return Num{V: n.V + x}
}

// Mul умножает
func (n Num) Mul(x int) Num {
        return Num{V: n.V * x}
}

func main() {
        t := Celsius(21.5)
        fmt.Println(t)
        fmt.Println(t.Round())
        n := Num{V: 1}
        fmt.Println(n.Add(2).Mul(3).V)
}
```

### Цепочка методов: метод возвращает свой тип

Цепочка вызовов работает тогда, когда метод возвращает значение того же типа, на котором вызван: 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() покажет прежнее имя: сеттер отбил невалидный ввод, чего не умело прямое присваивание полю.

```go
package main

import "fmt"

// Player — игрок с непубличным полем name
type Player struct {
        name string
}

// SetName записывает имя — приёмник-указатель
func (p *Player) SetName(name string) {
        p.name = name
}

// Name возвращает имя — приёмник-значение
func (p Player) Name() string {
        return p.name
}

func main() {
        p := Player{}
        p.SetName("аня")
        fmt.Println(p.Name())
        fmt.Println(p)
}
```

### Имя метода против имени поля

Поле и метод живут в разных мирах — но с одним общим пространством имён: у типа НЕ МОЖЕТ быть поля и метода с одним именем. 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-заданиях), но методы к нему — только снаружи, рядом с другими объявлениями пакета. Всё, что принадлежит типу, живёт в одном месте файла — и это не строгость ради строгости, а прямое следствие идеи «набор методов как меню типа»: меню должно быть просматриваемым целиком.

### Что выведет код: копия приёмника

```go
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)
}
```

**Что выведет программа?**

- ✅ 15 5: Added собрала новый счётчик из значения приёмника, оригинал c не менялся
- 15 15: правка в value-приёмнике доходит до оригинала
- 5 5: без приёмника-указателя метод вернул нулевой счётчик
- Программа не соберётся: метод не может возвращать структуру

Приёмник-значение получил копию c с N=5. Метод ничего не менял в приёмнике — он собрал НОВУЮ структуру Counter{N: 15} и вернул её; она записана в c2. Печать даёт c2.N — 15 и c.N — 5: оригинал цел. Это идиома «метод возвращает изменённую копию»: вместо правки состояния — новое значение, а старое живёт дальше. Метод может возвращать структуру наравне с любым типом. Пробник прогнал код на обоих движках — вывод одинаков: «15 5».

### Классика: value-приёмник пытается менять

```go
package 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

```go
package 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 решает, подходит ли тип. [Оглавление курса](/courses/go-basics/) — перед финалом самое время пробежаться по своим конспектам.

### Шпаргалка: методы

| Запись | Что делает | В песочнице |
| --- | --- | --- |
| 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».

```go
package main

import "fmt"

type Counter struct {
        N int
}

// Value возвращает значение — приёмник-значение
func (c Counter) Value() int {
        return c.N
}

// Inc увеличивает счётчик — приёмник-указатель
func (c *___) Inc() {
        c.N++
}

func main() {
        c := Counter{N: 2}
        p := ___c
        p.___()
        fmt.Println(c.Value(), p.Value())
}
```

Подсказка: Первый пропуск — имя типа после звёздочки в приёмнике: указатель на Counter. Второй — амперсанд при взятии адреса переменной. Третий — имя метода-модификатора, вызываемого на указателе.

Правильные ответы в пропусках: `Counter`, `&`, `Inc`

### Соберите программу: счётчик с методами

Соберите программу: тип Meter, два метода — value-метод-вопрос и pointer-метод-модификатор, — правка и печать. Собранный вывод — «20».

Правильный порядок строк:

1. `package main`
2. `import "fmt"`
3. `type Meter struct {`
4. `        M int`
5. `}`
6. `func (m Meter) Twice() int {`
7. `        return m.M * 2`
8. `}`
9. `func (m *Meter) Add(d int) {`
10. `        m.M += d`
11. `}`
12. `func main() {`
13. `        m := Meter{M: 7}`
14. `        m.Add(3)`
15. `        fmt.Println(m.Twice())`
16. `}`

> **Честно о песочнице: два движка.** Каждый ранимый пример урока прогнан пробником на обоих движках песочницы. Совпадают: парсинг приёмников обоих видов (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{...} либо без указателей.

### Карточки: методы

- **Как объявить метод и чем он отличается от функции?** — func (r Rect) Area() int { ... } — между func и именем стоит приёмник: имя и тип. Метод — та же функция, приёмник — её первый параметр; вызывается точкой на значении типа: r.Area(). Приёмник именуют коротко и одинаково у всех методов типа; this и self в Go нет. Перегрузки методов нет — одно имя на тип.
- **Что делает value receiver (приёмник-значение)?** — Метод получает КОПИЮ значения: правки полей внутри метода исчезают вместе с копией, оригинал цел. Дом для чистых методов-вопросов: Area(), Round(), Sum(). Маленькие и неизменяемые типы — его территория. В песочнице резервный движок копию не делает и протекает — в ранимых примерах value-методы поля не правят.
- **Что делает pointer receiver (приёмник-указатель)?** — func (c *Counter) Inc() — метод получает адрес оригинала и правит его через сахар (*c).N. Нужен для модификации, больших структур и согласованности набора методов. nil-приёмник возможен — обычные правила указателей из 22-го.
- **Почему c.Inc() работает без амперсанда, а p.Value() — без звёздочки?** — Вызов метода сглаживает оба направления: для pointer-метода на адресуемой переменной Go сам ставит (&c).Inc(); для value-метода на указателе — сам разыменовывает (*p).Value(). Граница — адресуемость: элемент карты m["a"].Inc() не соберётся (как и m["a"].X = 99 из 23-го), элемент среза cs[0].Inc() — соберётся.
- **Как объявить метод для float64?** — Напрямую — никак: методы объявляются только для типов вашего пакета. Путь: type Celsius float64 — новый именованный тип — и метод func (c Celsius) Round() int. Печать без String() показывает сырой float64: 21.5. Алиас type X = float64 новым типом не является — методов ему не прикрепить.
- **Что такое набор методов (method set) и зачем он нужен?** — Набор методов — все методы, объявленные для типа: «меню» того, что тип умеет. Value-методы входят в наборы T и *T, pointer-методы — только в *T. Именно набор методов решает, реализует ли тип интерфейс; поэтому приёмники одного типа держат согласованными — все value или все pointer. Геттер в Go — Name(), не GetName(); сеттер — SetName(); поле и метод не могут носить одно имя.

## Коротко: что запомнить

- Метод — функция с приёмником: 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 — сигнал искать неадресуемый приёмник.
