---
title: "Интерфейсы: контракты поведения"
description: "Интерфейсы в Go: неявная реализация без implements, fmt.Stringer, маленькие io.Reader и io.Writer, срез интерфейсов, var _ для проверки, ловушка nil."
course: "Продвинутые возможности Go: от замыканий до рефлексии"
url: "https://golangmaster.ru/courses/go-advanced/lessons/10-interfejsy/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 24
---

# Интерфейсы: контракты поведения

*Курс «Продвинутые возможности Go: от замыканий до рефлексии», урок 10 из 25. ~24 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/go-advanced/lessons/10-interfejsy/*

интерфейсы go — запрос, который вводят, когда методы уже освоены и хочется понять, как из них собираются контракты. Прошлый урок обещал мост: context.Context оказался интерфейсом с четырьмя методами, и мы обещали объяснить, почему тип удовлетворяет контракту, не объявляя об этом. Теперь исполняем обещание: интерфейс — это набор методов и контракт поведения, неявная реализация вместо implements, маленькие контракты вроде fmt.Stringer и io.Reader, полиморфизм через срез интерфейсов, compile-проверка var _ и честный разговор про nil-ловушку. Все примеры запускаются прямо на странице.

### Интерфейсы в Go: контракт поведения без объявлений

В [прошлом уроке](/courses/go-advanced/lessons/09-context/) мы научились проносить отмену и дедлайны по цепочке вызовов — и дважды обещали разобраться с одной фразой: **context.Context — интерфейс с четырьмя методами**. Что такое интерфейс, почему корень дерева и его дети прячутся под одним контрактом и почему тип удовлетворяет этому контракту, не объявляя об этом, — тема сегодняшняя. Это последний кирпич в фундаменте: после него фраза «интерфейс с четырьмя методами» перестанет быть заклинанием.

**Интерфейс в Go — это набор методов.** Не данные, не поведение по умолчанию, не иерархия классов — просто список сигнатур: «кто умеет вот это — годится». Такой список называют **контрактом поведения**: он описывает, *что* тип умеет делать, а не *чем* он является. `fmt.Stringer` говорит «умею печатать себя строкой»; `io.Reader` — «умею отдавать байты»; `context.Context` — «умею сообщать дедлайн, отмену, причину и значения». Конкретный тип попадает под контракт одним способом — реализуя все методы из списка. Как именно он их реализует и что у него внутри — контракту безразлично.

План урока: неявная реализация и контраст с Java, разбор `type Stringer interface` на живом примере, маленькие интерфейсы `io.Reader` и `io.Writer` как каркас стандартной библиотеки, полиморфизм через срез интерфейсов и параметры-интерфейсы, compile-проверка `var _ Interface = (*T)(nil)`, честная история про nil-интерфейс и nil внутри, и свой тип, который становится полноценным `io.Reader`. Интерфейсы — сильная сторона песочницы: почти все интерактивы дают одинаковый ответ в обоих движках; где есть исключения — честно отметим.

### Проверьте себя

**Вопрос:** Что нужно, чтобы тип Cat в Go стал fmt.Stringer (интерфейс из одного метода String() string)?

- Написать class Cat implements Stringer в заголовке объявления
- Наследовать Cat от базового класса Stringer
- ✅ Просто реализовать метод String() string у Cat — никакого объявления не нужно
- Зарегистрировать Cat в пакете fmt функцией fmt.Register

В Go реализация неявная: реализуйте методы контракта — и тип уже интерфейс. Ни объявления implements, ни наследования, ни регистрации не существует: компилятор сам видит, что набор методов Cat включает String() string, и считает Cat годным к fmt.Stringer. Ключ «implements» вообще не зарезервирован в языке — контракт поведения проверяется по фактам, а не по заявлениям.

### Неявная реализация: implements не нужен

Если вы приходите из Java или C#, самое странное в Go звучит так: **тип не объявляет, чей контракт он выполняет**. В Java пишут `class Cat implements Stringer` — компилятор проверяет заявление в заголовке, и без него тип не пройдёт там, где ждут интерфейс. В Go заявления нет: вы просто пишете метод `String() string` — и `Cat` уже можно присвоить переменной типа `Stringer`. Компилятор сверяет **наборы методов**: у контракта один список, у типа — свой; список типа покрывает список контракта — тип готов. Эту модель называют структурной типизацией, а в народе — утиной: «если он ходит как утка и крякает как утка — это утка». Родословная не спрашивается.

По запросу **interface golang** поисковая выдача ведёт на тур Go (go.dev/tour) и Effective Go, раздел Interfaces, — там канон сформулирован одной фразой: интерфейсы задают контракты, которые типы выполняют, не объявляя об этом. Что за этим стоит на практике? **Свобода чужого кода.** Типы из чужих пакетов — из стандартной библиотеки, из библиотек в Интернете — вы не редактируете: исходники закрыты для правок, а implements в Java надо дописать именно в исходник типа (или городить адаптер). В Go же контракт может появиться *после* типов: вы объявляете свой интерфейс с двумя методами — и внезапно оказывается, что под него уже подходят три типа из библиотек, о которых авторы интерфейса никогда не слышали. Контракт в Go — это заявление потребителя («дайте мне вот такое поведение»), а не заявление типа («я такой-то»).

Отсюда и разделение, которое вы уже чувствовали по всему курсу: [sync.Mutex](/courses/go-advanced/lessons/08-muteksy/) — **конкретный тип** со своими методами Lock и Unlock, у него есть внутренности (состояние, очередь ждущих); `io.Reader` — **интерфейс**, у него внутренних дел нет вообще, только сигнатуры. Конкретный тип можно инстанцировать и потрогать поля; интерфейс можно только требовать и вызывать. Вся стандартная библиотека построена на простом правиле: функции принимают интерфейсы (минимально узкие), а возвращают конкретные типы.

### type Stringer: один метод — и fmt печатает по-вашему

Разберём контракт вживую. Объявление интерфейса похоже на объявление структуры, только внутри — методы без тел:

`type Stringer interface { String() string }`

Читается так: «любой тип, у которого есть метод `String()`, возвращающий `string`, — это Stringer». В стандартной библиотеке живёт точно такой же контракт — `fmt.Stringer` из пакета fmt, — и у него особая честь: **fmt сам его вызывает**. Когда `fmt.Println`, `fmt.Printf` с глаголом `%v` и почти все печатные функции пакета получают значение, они проверяют: реализует ли оно `String() string`? Реализует — печатают результат этого метода вместо внутренностей. Так fmt печатает время как «2006-01-02», а не структуру из полей: `time.Time` просто реализует String.

Строительным блоком здесь служат **методы**, которые вы писали в основном курсе (урок о методах структур): приёмник-значение, приёмник-указатель, вызов через точку. Методы — единственное, что делает тип интерфейсом: нет методов — нет контракта; есть метод `String() string` — есть `Stringer`. Одно правило о приёмниках стоит освежить сразу: **набор методов значения и набор методов указателя различаются**. Метод с приёмником-значением есть и у значения, и у указателя (указатель всегда умеет разыменоваться); метод с приёмником-указателем есть только у указателя. Значит, `Cat` с `String()` на значении удовлетворяет `Stringer` и значением, и указателем, а тот же контракт на `*Cat` — только указателем. В ранимых блоках урока мы держим приёмники-значения — и контракт, и печать ведут себя одинаково предсказуемо.

### Stringer в деле: fmt зовёт String сам

Запустите: обе строки напечатаны методом String — «кот Барсик» и «кот Мурка», хотя мы не вызывали его руками. Первая строка: значение присвоено переменной типа fmt.Stringer, и fmt при печати вызвал String у динамического значения. Вторая: даже «сырая» структура попадает под тот же контракт — fmt проверяет набор методов конкретного типа. Эксперимент: переименуйте метод String в Speak — программа не соберётся: переменной fmt.Stringer больше ничего не подходит, и fmt перестанет звать метод. Один метод — весь контракт; переименовал — контракт потерян.

```go
package main

import "fmt"

// Cat — обычная структура, никакого implements не объявлено
type Cat struct {
        name string
}

// String — один метод, и Cat уже fmt.Stringer
func (c Cat) String() string {
        return "кот " + c.name
}

func main() {
        var s fmt.Stringer = Cat{name: "Барсик"}
        fmt.Println(s)                  // fmt замечает String() и зовёт его сам
        fmt.Println(Cat{name: "Мурка"}) // и без интерфейсной переменной тоже
}
```

### Маленькие интерфейсы: io.Reader и io.Writer

Stringer — пример важнейшего вкусового канона Go: **интерфейсы должны быть маленькими**. Чем меньше методов в контракте, тем больше типов ему удовлетворяют — и тем шире применение кода, который контракт принимает. Один метод — идеал: `Stringer`, `io.Reader`, `io.Writer`, `fmt.GoStringer`. Есть и легендарные двух-трёхметодные (`io.ReadWriteCloser`, `http.Handler`), но правило вкуса одно: объявляйте интерфейс тогда, когда вам нужно *поведение*, а не «всё подряд», и не раздувайте список «на будущее» — реализаций станет меньше, а пользы не прибавится. Каноническая цитата из доков Rob Pike: «чем больше интерфейс, тем слабее типизация» (The bigger the interface, the weaker the abstraction).

Главный контракт всего серверного Go — `io.Reader`. Вот его сигнатура целиком:

`Read(p []byte) (n int, err error)`

Три части: `p` — буфер, **который даёт вам** потребитель («вот память, заполняй»); `n` — сколько байт вы реально записали (может быть меньше длины буфера — и это норма, а не ошибка); `err` — ошибка, и особая её форма — `io.EOF`: «данные кончились». EOF — не катастрофа, а штатный сигнал конца потока, и весь код чтения в экосистеме крутится вокруг пары «прочитал n — проверь err». `io.Writer` — зеркальный контракт: `Write(p []byte) (n int, err error)` — «вот байты, возьми их, сколько взял — скажи». Файловая система, сеть, буферы в памяти, HTTP-тела, шифрование, сжатие — всё говорит на этих двух сигнатурах. Поэтому `fmt.Fprintf(w io.Writer, ...)` печатает хоть в файл, хоть в сеть, хоть в `strings.Builder`: функция не знает, *куда* она пишет, — она знает только, что туда можно `Write`. Это и есть контракт поведения в действии: потребитель объявляет минимальное поведение, а поставщиков — сотни.

### Два контракта, на которых стоит экосистема

```go
// io.Reader: «заполни мой буфер байтами из себя»
type Reader interface {
        Read(p []byte) (n int, err error)
}

// io.Writer: «возьми байты и запиши их в себя»
type Writer interface {
        Write(p []byte) (n int, err error)
}

// io.EOF — штатный сигнал конца данных, а не катастрофа:
// Read вернул (0, io.EOF) — данных больше не будет.
```

Это настоящие объявления из пакета io — упрощать нечего. Один метод на контракт, буфер передаёт потребитель, число записанных байт и ошибка — возврат. io.EOF — предобъявленное значение пакета; проверять его принято идиомой err == io.EOF для Reader-циклов.

### Полиморфизм: срез интерфейсов и параметры-интерфейсы

Теперь плоды. **Полиморфизм** — разное поведение у значений под одним контрактом — в Go делается без наследования и без виртуальных таблиц на виду: значение интерфейса хранит пару «динамический тип + значение», и вызов метода идёт к динамическому типу. Практическая фигура номер один — **срез интерфейсов**: `[]fmt.Stringer` или ваш собственный `[]Speaker`. В одной корзине лежат значения разных конкретных типов, и цикл `for` зовёт один и тот же метод — каждый раз свой. Добавить новый вид «говорящего» — значит написать новый тип с нужным методом; цикл и срез не меняются. Это расширение без правок старого кода — то самое, ради чего в других языках строят иерархии классов.

Фигура номер два — **интерфейс как параметр функции**. Функция `func hello(g Greetable) string` принимает любой тип с методом `Greet() string`: и кота, и человека, и робота. Внутри функции доступно только поведение из контракта — вызвать `Greet`, и всё; поля `name` у `Greetable` не существует, ведь у одного динамического типа оно есть, у другого — нет. Это ограничение — сила: функция, написанная против маленького контракта, работает сегодня с котом, завтра с HTTP-запросом, послезавтра с фейком из тестов.

Обратная сторона той же монеты: в **возвращаемых** значениях идиома наоборот — возвращайте конкретные типы, а не интерфейсы. Конкретный тип несёт вызывающему все свои методы и поля; интерфейс отрезал бы всё, кроме контракта, — без пользы, ведь вызывающий знает, что именно вернул его код. Правило доков: «будьте консервативны в том, что принимаете, либеральны в том, что возвращаете» — тут оно перевёрнуто относительно многих языков и проверено десятилетиями.

### Срез Speaker: три типа, один контракт

Запустите: цикл зовёт один и тот же метод Speak у трёх разных типов, и каждый отвечает по-своему — это полиморфизм без наследования. Эксперимент: допишите тип Bird с методом Speak и вставьте его в срез — ни цикл, ни интерфейс, ни остальные типы менять не придётся. Это и есть расширение через контракт: новый тип сам вписывается в корзину, потому что набор методов совпал.

```go
package main

import "fmt"

// Speaker — маленький интерфейс из одного метода
type Speaker interface {
        Speak() string
}

type Cat struct{ name string }

func (c Cat) Speak() string { return "кот " + c.name + ": мяу" }

type Dog struct{ name string }

func (d Dog) Speak() string { return "пёс " + d.name + ": гав" }

type Robot struct{ id string }

func (r Robot) Speak() string { return "робот " + r.id + ": бип" }

func main() {
        // в одном срезе — три РАЗНЫХ конкретных типа под одним контрактом
        zoo := []Speaker{Cat{name: "Барсик"}, Dog{name: "Шарик"}, Robot{id: "R2-D2"}}
        for _, s := range zoo {
                fmt.Println(s.Speak())
        }
}
```

### Интерфейс как параметр: что можно, а что нет

```go
package main

import "fmt"

type Greetable interface {
        Greet() string
}

type Cat struct{ name string }

func (c Cat) Greet() string { return "мяу, я " + c.name }

type Person struct{ name string }

func (p Person) Greet() string { return "привет, я " + p.name }

func hello(g Greetable) string {
        return "здравствуйте: " + g.Greet()
}

func main() {
        fmt.Println(hello(Cat{name: "Барсик"}))
        fmt.Println(hello(Person{name: "Анна"}))
}
```

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

- Программа не соберётся: в hello нельзя смешивать Cat и Person
- ✅ Две строки: «здравствуйте: мяу, я Барсик» и «здравствуйте: привет, я Анна»
- Паника: у Greetable нет поля name
- Две строки «здравствуйте: привет, я Анна» — вызовы сливаются

hello объявляет: «мне хватит поведения Greet() string» — и оба типа вписываются, компилятор сверил наборы методов в точках вызова. Внутри hello доступен только контракт: g.Greet() — да, g.name — нет (поле принадлежит конкретным типам, а не контракту; при попытке будет ошибка компиляции «g.name undefined», не паника). Оба вызова напечатают свои строки: динамический тип решает, чей Greet выполнится.

### var _ Interface = (*T)(nil): компилятор вместо теста

Неявная реализация имеет цену: в Java «интересно, реализует ли Cat интерфейс» видно по заголовку класса; в Go надо смотреть наборы методов. Когда тип далеко (в другом пакете) или контракт менялся, у Go есть идиома **compile-проверки** — заявление, которое живёт на уровне пакета и не даёт контракту тихо расползтись:

`var _ fmt.Stringer = Robot{}`

Читается: «переменная-пустышка _ типа fmt.Stringer получает значение Robot». Компилятор обязан проверить присваивание: если набор методов Robot не покрывает fmt.Stringer — программа не соберётся, прямо здесь, а не в тесте на проде. Пустой идентификатор `_` выбрасывает значение, ничего в памяти не живёт, строчки кода исполнения не занимает — чистое заявление контракта.

В исходниках стандартной библиотеки чаще встречается форма с указателем:

`var _ fmt.Stringer = (*Robot)(nil)`

Зачем через `(*T)(nil)`? Она проверяет **указательный** набор методов: тип с приёмниками-указателями обязан покрыть контракт указателем, и конверсия `(*Robot)(nil)` — создание nil-указателя Robot без единого выделения памяти. Если String написан на значении, а контракт требуют у `*Robot` — компилятор ругнётся немедленно. Обе формы — норма продакшена: в пакетах stdlib такие строки стоят рядом с объявлением типа как страховка. И одна честная нота про песочницу: форму `(*Robot)(nil)` резервный движок не парсит (основной — компилирует), поэтому в живом блоке ниже — форма со значением, а указательная — в статичном блоке здесь.

### Идиома compile-проверки как её пишут в проде

```go
package bookstore

import "fmt"

type Book struct{ Title string }

func (b Book) String() string { return "«" + b.Title + "»" }

// compile-проверка: контракт живёт, даже если методы уедут в рефакторинг
var _ fmt.Stringer = Book{}       // форма со значением
var _ fmt.Stringer = (*Book)(nil) // форма с указателем — как в исходниках stdlib
```

Строки с var _ ставят рядом с объявлением типа. Они не создают ничего в рантайме: компилятор проверяет присваивание и выбрасывает пустышку. Правая форма проверяет указательный набор методов и не выделяет память. Резервный движок песочницы вторую форму не парсит — сверяйте её локальным go build.

### Проверьте себя ещё раз

**Вопрос:** var c *Cat = nil; var s fmt.Stringer = c. Чему равно s == nil?

- true: внутри s лежит nil, значит интерфейс nil
- ✅ false: в паре (динамический тип, значение) тип заполнен — *Cat, а nil лежит только на месте значения
- true, если у Cat нет других методов
- Компилятор не даст присвоить nil-указатель интерфейсу

Классическая ловушка typed-nil. Интерфейс — пара «динамический тип + значение»; nil-интерфейс — пара (nil, nil). После присваивания nil-указателя пара становится (*Cat, nil): тип заполнен, поэтому s == nil — false, хотя внутри nil. Именно поэтому в Go принято возвращать ошибку как return nil, а не return (*MyErr)(nil). Ловушка упоминается здесь честно и базово; глубже разберём её в одиннадцатом и двенадцатом уроках. Резервный движок песочницы на этот вопрос отвечает неверно (говорит true) — настоящий Go и основной движок говорят false.

### nil-интерфейс: что стоит за нулевым значением

Раз ловушка Typed-nil всплыла — аккуратно разложим nil-словарь интерфейсов, без которых в Go не прожить. **Нулевое значение интерфейса — nil**, и это пара из двух nil: ни динамического типа, ни значения. `var s fmt.Stringer` — именно она. Проверка `s == nil` возвращает true, вызов метода на такой переменной роняет программу с паникой «nil pointer dereference» — контракту нечего вызывать, динамического типа нет. Правило простое: прежде чем звать метод, убедитесь, что переменной что-то присвоено — или проверьте на nil, если она приходит извне.

Как только интерфейсу присвоили значение — любая nil-пара исчезает: присваивание `s = Cat{}` кладёт пару (Cat, значение), и `s == nil` уже false, даже если сам Cat внутри нулевой. Отсюда правило для авторов функций, возвращающих интерфейсы (чаще всего — `error`, который тоже интерфейс из одного метода `Error() string`): **возвращайте голый nil как nil**. Строчка `return nil` кладёт пару (nil, nil) — вызывающий увидит err == nil и спокойно пойдёт дальше. А вот `var e *MyErr = nil; return e` кладёт пару (*MyErr, nil) — err не равен nil, хотя внутри ничего нет, и проверка вызывающего обманута. Это и есть ловушка Typed-nil целиком; сегодня достаточно узнавать её в лицо и держать привычку `return nil`.

И симметричная деталь, которую стоит знать базово: интерфейс с заполненным типом, но nil-значением внутри — не то же самое, что nil-интерфейс, и методы на нём *иногда* вызываются спокойно (метод с приёмником-указателем, который не трогает поля, отработает), а иногда паникуют (разыменование nil внутри метода). Не полагайтесь на «повезёт»: пустые указатели под контрактом — ошибка проектирования, а не фича. В одиннадцатом и двенадцатом уроках вернёмся к этому сюжету с инструментами — пустым интерфейсом any и type assertion-ом, которые позволяют отличать динамические типы честно.

### nil-интерфейс до и после присваивания

```go
package main

import "fmt"

type Cat struct{ name string }

func (c Cat) String() string { return "кот " + c.name }

func main() {
        var s fmt.Stringer
        fmt.Println("до присваивания s == nil:", s == nil)
        s = Cat{name: "Барсик"}
        fmt.Println("после присваивания s == nil:", s == nil)
}
```

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

- true и true: Cat нигде не объявлен как Stringer
- ✅ true и false: нулевой интерфейс nil, присваивание заполняет пару
- false и false: переменная интерфейсного типа всегда не nil
- Программа не соберётся: сравнивать интерфейс с nil нельзя

var s fmt.Stringer — пара (nil, nil): сравнение с nil даёт true. Присваивание s = Cat{} кладёт динамический тип Cat и значение — пара заполнена, s == nil уже false. Сравнение интерфейса с nil законно и точно: оно проверяет, что пара пуста целиком. Обратите внимание: false здесь потому, что тип И значение заполнились; nil-указатель заполнил бы только тип — false без «жизни внутри» (ловушка из quiz-а).

### Свой io.Reader: контракт как разъём

Финальный номер — собрать собственный `io.Reader`. Зачем? Затем, что `io.Reader` — это **разъём**: любой тип с методом `Read(p []byte) (int, error)` втыкается в любую функцию, которая читает. Источники — файлы, сеть, строки — вам знакомы; самое вкусное начинается, когда контракт используется для **декораторов**: тип, который сам является Reader-ом и внутри себя держит другой Reader, читая через него и преобразуя данные. Так устроены bufio.Reader, gzip.Reader, crypto-обёртки — всё держится на том, что внешняя обёртка и внутренний источник говорят на одной сигнатуре.

Соберём `shoutReader`: структура с полем `src io.Reader` (уже контракт! — источник может быть чем угодно), метод Read читает порцию из источника, поднимает строчные латинские буквы в верхний регистр и отдаёт наружу с результатом источника — включая io.EOF в конце. Источник возьмём `strings.NewReader` — Reader из строки, он же удобен в тестах. В main переменная `var r io.Reader` принимает нашего шаутера — и дальше с ним можно делать всё, что делается с любым Reader: цикл чтения до io.EOF, передача в io.Copy, в http-ответ. Программа не знает, что источник — самодельный: контракт обеспечил совместимость. Это и есть «методы делают тип интерфейсом» на максимальной громкости: один метод Read — и ваш тип втянут в экосистему из сотен функций.

### shoutReader: свой тип в экосистеме io

Запустите: наш самодельный Reader втыкается в честный io-цикл — буфер buf на десять байт, чтение до io.EOF, сборка через append. На выходе всё содержание источника, но ПРИПЕЧАТАННОЕ: shoutReader преобразовал каждую порцию, прежде чем отдать её наружу. io.EOF напечатан как штатный конец данных. Эксперимент: уменьшите буфер до 4 байт — результат не изменится ни на букву: цикл Read/EOF собирает поток порциями любого размера, потому контракт и хорош.

```go
package main

import (
        "fmt"
        "io"
        "strings"
)

// shoutReader — свой тип: читает источник и ВОЗВЁЛ всё в верхний регистр.
// Метод Read(p []byte) (int, error) — и это уже io.Reader.
type shoutReader struct {
        src io.Reader
}

func (s shoutReader) Read(p []byte) (int, error) {
        n, err := s.src.Read(p)
        for i := 0; i < n; i++ {
                if p[i] >= 'a' && p[i] <= 'z' {
                        p[i] = p[i] - 32
                }
        }
        return n, err
}

func main() {
        var r io.Reader = shoutReader{src: strings.NewReader("quiet text via io.Reader")}
        var all []byte
        buf := make([]byte, 10)
        for {
                n, err := r.Read(buf)
                all = append(all, buf[:n]...)
                if err == io.EOF {
                        break
                }
        }
        fmt.Println("на выходе:", string(all))
        fmt.Println("конец данных:", io.EOF)
}
```

### Шпаргалка: каркас интерфейсов

| Инструмент | Что это | Правило | Осторожно |
| --- | --- | --- | --- |
| type Stringer interface { ... } | свой контракт: список методов без тел | реализуй все методы — тип уже интерфейс, implements не нужен | маленькие интерфейсы лучше: один метод — идеал |
| fmt.Stringer | контракт из одного метода String() string | fmt.Println/Printf зовут String сами и печатают её результат | переименовал метод — потерял контракт, компилятор var _ подстрахует |
| io.Reader | Read(p []byte) (n int, err error) | буфер даёт потребитель; n — сколько заполнил; io.EOF — штатный конец | n может быть меньше len(p) — это норма, проверяй err после n |
| io.Writer | Write(p []byte) (n int, err error) | зеркальный контракт: «возьми байты и запиши» | возврати n и err честно: частичная запись — не повод молчать |
| var _ fmt.Stringer = (*T)(nil) | compile-проверка контракта | стоит рядом с типом; не создаёт ничего в рантайме | указательная форма — в основном движке; резервный её не парсит |
| nil-интерфейс | пара (динамический тип, значение) из двух nil | s == nil true только пока пара пуста; return nil — всегда голый nil | typed-nil: (*T)(nil) под контрактом — НЕ nil-интерфейс |

### Правила гигиены интерфейсов

- Держите интерфейсы маленькими: один-два метода. Контракт объявляет потребитель, а не тип — принимайте поведение, а не «всё подряд»; чем больше интерфейс, тем меньше реализаций.
- Неявная реализация — не отсутствие дисциплины: ставьте var _ Interface = (*T)(nil) рядом с типом, и компилятор охраняет контракт бесплатно, без тестов и рантайма.
- Приёмники держите единообразно: методы одного типа — все на значении или все на указателе; помните, что контракт с приёмником-указателем удовлетворяет только *T.
- Функции принимают интерфейсы, возвращают конкретные типы; возвращая интерфейс (в том числе error), возвращайте голый nil — typed-nil ломает проверку вызывающего.
- Чтение через io.Reader — всегда цикл «Read → проверить err == io.EOF»; n меньше длины буфера — норма; для источников-строк в тестах берите strings.NewReader.

> **Честно о двух движках песочницы.** Интерфейсы — счастливая тема: почти все интерактивы урока (Stringer-раннер, срез Speaker, оба predict-а, fill и order) дают одинаковый результат в обоих движках — основном (настоящий Go, yaegi в WASM) и резервном упрощённом. Исключений три, и все честные. Первое: пакет io в резервном движке недоступен — при импорте он отвечает «пакет "io" недоступен в песочнице (доступны: fmt, strings, strconv, math, sort, errors, time)», поэтому shoutReader-раннер проверяйте в основном движке. Второе: форма (*Robot)(nil) в резервном не парсится — compile-проверка с указателем живёт в статичном блоке и локальном go build. Третье, самое хитрое: резервный fmt подставляет String() даже к значениям под собственными интерфейсами урока, а основной зовёт String() у конкретных типов и fmt.Stringer, но печатает фигурные скобки для собственных интерфейсных типов — поэтому во всех ранимых блоках урока мы зовём String() явно или держим значение в переменной типа fmt.Stringer, и оба движка совпадают.

### Впишите скелет контракта

Восстановите минимальную программу с fmt.Stringer: имя метода контракта, имя интерфейса и явный вызов метода — по одному слову на пропуск.

```go
package main

import "fmt"

type Robot struct {
        id string
}

func (r Robot) ___() string { return "робот " + r.id }

var _ fmt.___ = Robot{}

func main() {
        var s fmt.Stringer = Robot{id: "R2"}
        fmt.Println(s.___())
}
```

Подсказка: Три слова из одного корня: метод контракта, сам контракт из пакета fmt и явный вызов метода через переменную.

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

### Соберите полиморфный зоопарк

Соберите программу: два типа с методом String, срез fmt.Stringer и цикл, который печатает каждого — вызов метода в цикле оставьте fmt-у, он сам зовёт String.

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

1. `package main`
2. `import "fmt"`
3. `type Cat struct{ name string }`
4. `func (c Cat) String() string { return "кот " + c.name }`
5. `type Robot struct{ id string }`
6. `func (r Robot) String() string { return "робот " + r.id }`
7. `func main() {`
8. `zoo := []fmt.Stringer{Cat{name: "Барсик"}, Robot{id: "R2"}}`
9. `for _, s := range zoo {`
10. `fmt.Println(s)`
11. `}`
12. `}`

### Что дальше: контракт как стиль мышления

Оглянемся на четыре урока подряд: [каналы и горутины](/courses/go-advanced/lessons/04-gorutiny/) научили программы общаться, [мьютексы](/courses/go-advanced/lessons/08-muteksy/) — не мешать друг другу, [контекст](/courses/go-advanced/lessons/09-context/) — вовремя останавливаться, а сегодня интерфейсы научили код **договариваться**: функции объявляют минимальное поведение, типы выполняют контракты неявно, и всё это собирается в срезы, параметры и разъёмы вроде io.Reader. У вас есть и страховка — `var _ Interface = (*T)(nil)` — и фильтр для ловушки typed-nil. [Весь курс](/courses/go-advanced/) с этого места разворачивается от механики к стилю.

Следующий шаг — [«Полиморфизм и пустой интерфейс»](/courses/go-advanced/lessons/11-polimorfizm/): одиннадцатый урок заберётся внутрь пары «тип + значение», познакомит с `any` — контрактом без единого метода, которому удовлетворяет всё, — и покажет, где гибкость становится ценой: type safety, срезы интерфейсов в больших программах и границы применимости. После него ловушка Typed-nil из сегодняшнего quiz-а получит полное вооружение — type assertion и переключение по типу, это территория [двенадцатого урока](/courses/go-advanced/lessons/12-type-assertion/). Интерфейсный фундамент, на котором стоит всё серверное Go — от `io.Reader` до `context.Context` — у вас теперь на руках.

### Карточки урока

- **Интерфейс в Go — что это** — Набор методов без тел — контракт поведения. Описывает, ЧТО тип умеет, а не ЧЕМ он является. Тип удовлетворяет контракту, реализовав все методы; implements и наследования не существует — реализация неявная (структурная, «утиная»).
- **Неявная реализация — чем отличается от Java** — Java: class Cat implements Stringer — заявление в заголовке обязательно. Go: заявления нет; компилятор сверяет набор методов типа с набором контракта. Плюс: чужие типы (без правки исходников) подходят под ваши интерфейсы, объявленные задним числом.
- **fmt.Stringer** — Интерфейс из одного метода String() string. fmt.Println/Printf вызывают его сами и печатают результат — так время печатается читаемо. Переименовал метод — потерял контракт; страховка — var _ fmt.Stringer = (*T)(nil).
- **io.Reader и io.Writer** — Read(p []byte) (n int, err error) — «заполни мой буфер»; Write(p []byte) (n int, err error) — «возьми мои байты». Буфер даёт потребитель, n — сколько реально обработано, io.EOF — штатный конец. Чтение — всегда цикл с проверкой err.
- **var _ Interface = (*T)(nil)** — Compile-проверка контракта: пустая переменная выбрасывается, компилятор проверяет присваивание. Указательная форма проверяет набор методов *T без выделения памяти. Резервный движок песочницы эту форму не парсит — основной компилирует.
- **nil-интерфейс и typed-nil** — Интерфейс — пара (динамический тип, значение). var s Stringer — (nil, nil), s == nil true, вызов метода — паника. Присвоение (*Cat)(nil) даёт (*Cat, nil) — уже НЕ nil (ловушка Typed-nil). Возвращайте голый return nil, глубже — в уроках 11 и 12.

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

- Интерфейс — набор методов и контракт поведения: описывает, что тип умеет, а не чем является; реализация неявная — implements не нужен, компилятор сверяет наборы методов.
- type Stringer interface { String() string } — шаблон маленького интерфейса; fmt.Stringer из stdlib fmt зовёт сам: Println и Printf печатают результат String().
- Методы — строительный блок интерфейсов: набор методов значения и указателя различается; контракт с приёмником-указателем удовлетворяет только *T.
- Маленькие интерфейсы правят экосистемой: io.Reader (Read(p []byte) (n int, err error)) и io.Writer — разъёмы, которыми соединены файлы, сеть и память; io.EOF — штатный конец потока.
- Полиморфизм без наследования: срез []fmt.Stringer хранит разные конкретные типы, цикл зовёт один метод — каждый тип отвечает по-своему; функции принимают интерфейсы и возвращают конкретные типы.
- var _ Interface = (*T)(nil) — compile-проверка: пустышка не живёт в рантайме, но роняет сборку при потере контракта; указательная форма проверяет набор методов *T.
- Интерфейс — пара (динамический тип, значение): var s Stringer — nil-пара, s == nil true; (*Cat)(nil) под контрактом — уже не nil (ловушка Typed-nil), возвращайте голый return nil.
- Свой тип с методом Read(p []byte) (int, error) — полноценный io.Reader: декоратор shoutReader втыкается в любой io-цикл и передаёт io.EOF дальше.

## Домашнее задание

Соберите мини-экосистему контрактов. Первый шаг: типы Book («Книга: ...»), Magazine («Журнал: ...»), Audio («Аудио: ...») с методом String() string и срез []fmt.Stringer, который циклом печатает весь каталог — добавьте четвёртый тип и убедитесь, что цикл не менялся. Второй шаг: поставьте у каждого типа compile-проверку var _ fmt.Stringer = ... (и одну указательную форму в отдельном файле, соберите локальным go build). Третий шаг: свой io.Reader — тип с полем src io.Reader, который прибавляет к каждой прочитанной порции счётчик байт и на io.EOF печатает итог; источники — strings.NewReader и os.File (обе втыкаются без изменений кода декоратора). Четвёртый шаг: проверьте ловушку — функция, возвращающая (*MyErr)(nil) как error, и убедитесь через == nil, что err не nil; перепишите на return nil. Комментарии в коде — по-русски, размер интерфейсов — один метод на контракт.

## Вопросы и ответы по уроку

### Зачем Go отказался от implements?

Неявная реализация разворачивает зависимость: контракт объявляет потребитель («мне нужно вот такое поведение»), а не тип («я такой-то»). Чужие типы — из stdlib или внешних библиотек — подходят под ваши интерфейсы без правки их исходников, а интерфейс можно объявить после типов, под которые он уже подходит. Компилятор по-прежнему проверяет совместимость строго — в точках присваивания и вызовов.

### Почему io.Reader требует передавать буфер, а не возвращает данные?

Буфер выделяет потребитель и переиспользует его в цикле: Read заполняет чужую память, а не аллоцирует новую на каждую порцию. Для горячих циклов чтения это экономит тысячи аллокаций. n сообщает, сколько байт реально записано (может быть меньше len(p)), err==io.EOF — штатный конец потока.

### Как убедиться, что мой тип не потерял контракт после рефакторинга?

Идиома compile-проверки: var _ fmt.Stringer = (*T)(nil) рядом с объявлением типа. Строка не создаёт значений в рантайме, но сборка падает, если набор методов перестал покрывать интерфейс. Указательная форма дополнительно гарантирует, что контракт покрыт именно *T — как это принято в исходниках стандартной библиотеки.

### Чем nil-интерфейс отличается от интерфейса с nil внутри?

Интерфейс хранит пару «динамический тип + значение». nil-интерфейс — (nil, nil): s == nil true, вызов метода паникует. Присвоенный (*Cat)(nil) даёт пару (*Cat, nil): s == nil уже false, хотя значения нет. Это ловушка Typed-nil; поэтому функции возвращают ошибки как голый return nil, а глубже тему разберут уроки про any и type assertion.
