Содержание урока · 14 разделов
- Интерфейсы в Go: контракт поведения без объявлений
- Неявная реализация: implements не нужен
- type Stringer: один метод — и fmt печатает по-вашему
- Маленькие интерфейсы: io.Reader и io.Writer
- Полиморфизм: срез интерфейсов и параметры-интерфейсы
- var _ Interface = (*T)(nil): компилятор вместо теста
- nil-интерфейс: что стоит за нулевым значением
- Свой io.Reader: контракт как разъём
- Шпаргалка: каркас интерфейсов
- Правила гигиены интерфейсов
- Что дальше: контракт как стиль мышления
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Интерфейсы в Go: контракт поведения без объявлений#
В прошлом уроке мы научились проносить отмену и дедлайны по цепочке вызовов — и дважды обещали разобраться с одной фразой: 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)?
Неявная реализация: 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 — конкретный тип со своими методами 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 перестанет звать метод. Один метод — весь контракт; переименовал — контракт потерян.
Запустите код, чтобы увидеть вывод.
Маленькие интерфейсы: 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 и вставьте его в срез — ни цикл, ни интерфейс, ни остальные типы менять не придётся. Это и есть расширение через контракт: новый тип сам вписывается в корзину, потому что набор методов совпал.
Запустите код, чтобы увидеть вывод.
Интерфейс как параметр: что можно, а что нет
Прочитайте код и выберите, что он выведет:
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: "Анна"}))
}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-проверки как её пишут в проде
GOpackage 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?
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-интерфейс до и после присваивания
Прочитайте код и выберите, что он выведет:
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)
}Свой 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 собирает поток порциями любого размера, потому контракт и хорош.
Запустите код, чтобы увидеть вывод.
Шпаргалка: каркас интерфейсов#
| Инструмент | Что это | Правило | Осторожно |
|---|---|---|---|
| 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: имя метода контракта, имя интерфейса и явный вызов метода — по одному слову на пропуск.
Соберите полиморфный зоопарк
Соберите программу: два типа с методом String, срез fmt.Stringer и цикл, который печатает каждого — вызов метода в цикле оставьте fmt-у, он сам зовёт String.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Что дальше: контракт как стиль мышления#
Оглянемся на четыре урока подряд: каналы и горутины научили программы общаться, мьютексы — не мешать друг другу, контекст — вовремя останавливаться, а сегодня интерфейсы научили код договариваться: функции объявляют минимальное поведение, типы выполняют контракты неявно, и всё это собирается в срезы, параметры и разъёмы вроде io.Reader. У вас есть и страховка — var _ Interface = (*T)(nil) — и фильтр для ловушки typed-nil. Весь курс с этого места разворачивается от механики к стилю.
Следующий шаг — «Полиморфизм и пустой интерфейс»: одиннадцатый урок заберётся внутрь пары «тип + значение», познакомит с any — контрактом без единого метода, которому удовлетворяет всё, — и покажет, где гибкость становится ценой: type safety, срезы интерфейсов в больших программах и границы применимости. После него ловушка Typed-nil из сегодняшнего quiz-а получит полное вооружение — type assertion и переключение по типу, это территория двенадцатого урока. Интерфейсный фундамент, на котором стоит всё серверное Go — от io.Reader до context.Context — у вас теперь на руках.
Карточки урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Интерфейс — набор методов и контракт поведения: описывает, что тип умеет, а не чем является; реализация неявная — 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.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Fiber: экспресс-подобный веб-фреймворк на Fasthttp
Курс Fiber для Go: маршруты, middleware, JSON, CORS, WebSocket, шаблоны, лимитеры и тестирование. 25 уроков с примерами кода и итоговым REST API.
- Поднимать HTTP-сервер на Fiber за минуты
- Проектировать маршруты, группы и версии API
- Писать middleware: логи, CORS, лимиты, recovery
- Принимать и отдавать JSON с валидацией