Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 10 из 25 ~24 мин +50 XP

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

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

Содержание урока · 14 разделов

Интерфейсы в 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-проверки как её пишут в проде

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?

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() stringfmt.Println/Printf зовут String сами и печатают её результатпереименовал метод — потерял контракт, компилятор var _ подстрахует
io.ReaderRead(p []byte) (n int, err error)буфер даёт потребитель; n — сколько заполнил; io.EOF — штатный конецn может быть меньше len(p) — это норма, проверяй err после n
io.WriterWrite(p []byte) (n int, err error)зеркальный контракт: «возьми байты и запиши»возврати n и err честно: частичная запись — не повод молчать
var _ fmt.Stringer = (*T)(nil)compile-проверка контрактастоит рядом с типом; не создаёт ничего в рантаймеуказательная форма — в основном движке; резервный её не парсит
nil-интерфейспара (динамический тип, значение) из двух nils == nil true только пока пара пуста; return nil — всегда голый niltyped-nil: (*T)(nil) под контрактом — НЕ nil-интерфейс

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

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

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

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

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.()) }

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

Соберите программу: два типа с методом 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 — у вас теперь на руках.

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

Термин

Интерфейс в Go — что это

нажмите, чтобы перевернуть
Определение

Набор методов без тел — контракт поведения. Описывает, ЧТО тип умеет, а не ЧЕМ он является. Тип удовлетворяет контракту, реализовав все методы; implements и наследования не существует — реализация неявная (структурная, «утиная»).

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 Pro · Интерфейсы: контракты поведения)

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

Зачем 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.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Fiber25 уроков~13 чпрактика25 уроков уже доступны

Fiber: экспресс-подобный веб-фреймворк на Fasthttp

Курс Fiber для Go: маршруты, middleware, JSON, CORS, WebSocket, шаблоны, лимитеры и тестирование. 25 уроков с примерами кода и итоговым REST API.

  • Поднимать HTTP-сервер на Fiber за минуты
  • Проектировать маршруты, группы и версии API
  • Писать middleware: логи, CORS, лимиты, recovery
  • Принимать и отдавать JSON с валидацией