Урок курса

Интерфейсы: определение, неявная реализация и type assertion

Golang с нуля

В прошлом уроке мы дали Counter набор методов с pointer-receiver — Increment, Reset, Value. Теперь посмотрим, как именно этот набор превращается в нечто большее: тип автоматически начинает «удовлетворять» интерфейсу, не сообщая об этом явно ни компилятору, ни читателю кода.

Объявление интерфейса и неявная реализация через совпадение сигнатур

Интерфейс в Go — это просто перечень сигнатур методов. Никакой реализации, никаких полей.

type Stringer interface {
    String() string
}

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

Определим два типа:

type Dog struct{ Name string }
type Cat struct{ Name string }

func (d Dog) String() string { return "Dog: " + d.Name }
func (c Cat) String() string { return "Cat: " + c.Name }

Оба типа реализуют Stringer — компилятор проверяет это в момент присваивания или передачи в функцию. Если сигнатуры совпадают, всё работает; если нет — ошибка компиляции с точным указанием, какого метода не хватает.

Теперь важный нюанс, который мы уже встречали при разборе pointer-receiver. Посмотри на такой вариант:

type Counter struct{ n int }

func (c *Counter) String() string {
    return fmt.Sprintf("counter=%d", c.n)
}

Здесь метод объявлен на *Counter, а не на Counter. Значит, интерфейс Stringer реализует тип *Counter — именно указатель. Значение Counter (не указатель) этому интерфейсу не удовлетворяет:

var s Stringer

s = &Counter{}  // OK: *Counter реализует Stringer
s = Counter{}   // ошибка компиляции: Counter не реализует Stringer

Правило простое: набор методов T включает только те методы, у которых receiver — T. Набор методов *T включает методы и с receiver T, и с receiver *T. Поэтому *T всегда реализует как минимум то, что реализует T, но не наоборот.

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

Присваивание конкретных типов переменной интерфейса и диспетчеризация метода

Когда переменная объявлена с интерфейсным типом, ей можно присвоить любое значение, чей конкретный тип реализует этот интерфейс:

var s Stringer
s = Dog{Name: "Rex"}
fmt.Println(s.String()) // Dog: Rex

s = Cat{Name: "Murka"}
fmt.Println(s.String()) // Cat: Murka

При вызове s.String() Go смотрит не на тип переменной s (он всегда Stringer), а на конкретный тип, который в ней хранится прямо сейчас. Это и есть диспетчеризация во время выполнения: каждый раз выбирается метод именно того типа, который лежит внутри.

Главный смысл интерфейса проявляется в функциях:

func printAll(items []Stringer) {
    for _, item := range items {
        fmt.Println(item.String())
    }
}

printAll не знает ничего о Dog и Cat. Она работает с любым значением, у которого есть метод String() string. Передадим оба типа вместе:

func main() {
    animals := []Stringer{
        Dog{Name: "Rex"},
        Cat{Name: "Murka"},
        Dog{Name: "Buddy"},
    }
    printAll(animals)
}

Вывод:

Dog: Rex
Cat: Murka
Dog: Buddy

Каждый вызов item.String() внутри цикла разрешается в метод именно того конкретного типа, который находится в item на этой итерации. Срез []Stringer хранит значения разных типов, и это не требует ни приведения, ни дополнительных проверок — компилятор гарантирует, что у каждого элемента есть нужный метод.

Одна и та же переменная s сначала указывала на Dog, потом на Cat. Это не переопределение типа переменной — тип s всегда Stringer. Меняется только конкретное значение внутри.

Type assertion и пустой интерфейс any

Иногда нужно сделать обратное: получить из интерфейсной переменной конкретное значение определённого типа. Для этого есть type assertion.

var s Stringer = Dog{Name: "Rex"}
d := s.(Dog)
fmt.Println(d.Name) // Rex

s.(Dog) говорит: «я знаю, что внутри Dog — дай мне его». Если это так, d получает конкретное значение Dog. Если нет — программа завершается паникой вида:

interface conversion: main.Stringer is main.Cat, not main.Dog

(точный текст зависит от пакета и конкретных типов, но суть одна: Go сообщает, что ожидался один тип, а внутри оказался другой.)

Паника — плохой исход, когда тип заранее неизвестен. Поэтому есть безопасная форма с двумя возвращаемыми значениями:

d, ok := s.(Dog)
if ok {
    fmt.Println("это Dog:", d.Name)
} else {
    fmt.Println("это не Dog")
}

При ok == false переменная d получает нулевое значение типа Dog (пустую структуру), паники нет. Это стандартный идиоматичный способ проверить конкретный тип, когда ты не уверен.


Теперь про any. Это псевдоним interface{} — интерфейс без единого метода. Раз методов нет, любой тип автоматически его реализует. Переменная типа any принимает что угодно:

var x any
x = 42
x = "hello"
x = Dog{Name: "Rex"}

fmt.Println принимает аргументы именно как any (точнее, ...any), поэтому туда можно передать значение любого типа.

Однако у переменной типа any нет никаких предметных операций — компилятор видит только статический тип any и не разрешит, например, арифметику или вызов пользовательского метода без уточнения типа:

var x any = 100
fmt.Println(x + 1) // ошибка компиляции: operator + not defined on interface {}

Чтобы работать с конкретным значением, нужна type assertion. Безопасная форма сохраняет полный контроль:

var x any = 100
if n, ok := x.(int); ok {
    fmt.Println(n * 2) // 200
}

Однозначная форма без ok вернёт панику в рантайме, если динамический тип не совпадёт:

var x any = 100
s := x.(string) // panic: interface conversion: interface {} is int, not string
_ = s

Компилятор здесь промолчит — он не знает, что именно лежит внутри any во время выполнения. Ошибка появится только в рантайме.

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

Задача 1

Тип Cat объявлен так:

type Cat struct{ Name string }
func (c Cat) String() string { return c.Name }

Интерфейс: type Stringer interface{ String() string }. Какие присваивания корректны?

var s Stringer = Cat{Name: "Murka"}   // A
var s Stringer = &Cat{Name: "Murka"}  // B
Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Что произойдёт при выполнении следующего кода?

var s Stringer = Dog{Name: "Rex"}
cat, ok := s.(Cat)

Где Stringer — интерфейс с методом String() string, Dog и Cat — разные типы, оба реализуют Stringer.

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Для чего используется тип any (псевдоним interface{})?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 4

Квитанции способов оплаты

Любой кассир знает: в конце смены важно не просто зафиксировать, сколько денег поступило, но и понять, каким именно способом был проведён каждый платёж. Наличные и купоны — совершенно разные инструменты расчёта, хотя и те и другие гасят долг покупателя. Когда их нужно обработать единообразно, возникает желание собрать все квитанции в одну стопку и пройтись по ней, не думая о том, что именно лежит под каждым листком.

В языке Go такую «стопку» удобно представить срезом, элементы которого объединены общим интерфейсом. Каждый тип платежа самостоятельно описывает себя через метод Summary, и срез принимает любой из них без явного указания конкретного типа. Это позволяет пройти по всем квитанциям в одном цикле и напечатать краткое описание каждой — вне зависимости от того, за чем скрывается значение.

Что нужно сделать

Допишите краткое описание для Cash и Coupon, создание квитанции по виду платежа и безопасную проверку последнего элемента. Программа должна хранить обе квитанции как Receipt, вывести описание каждой, а затем показать скидку последнего платежа, только если это купон. Вход содержит только виды cash и coupon.

Формат ввода

Две строки: вид платежа и целое число. Для cash это сумма, для coupon — скидка.

Формат вывода

Сначала по одной строке на квитанцию: cash=N или coupon=N. Последняя строка: discount=N для купона либо discount=none для другого вида платежа.

Пример

Ввод:
cash 120
coupon 15

Вывод:
cash=120
coupon=15
discount=15
РешениеGo

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 5

Этикетки для посылок

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

Работник склада собирает очередную партию в общий список, не разбирая заранее, что перед ним — обычная коробка или стеклянный сервиз в пузырчатой плёнке. Его задача — пройти по всему списку в том порядке, в котором отправления были приняты, и для каждого распечатать нужный комплект строк. При этом важно не вскрывать упаковку, чтобы выяснить тип содержимого: достаточно спросить у самого отправления, умеет ли оно сообщать о своей хрупкости.

Именно так устроена программа: она хранит все отправления вместе и последовательно обращается к каждому за основной этикеткой, а затем аккуратно проверяет, способно ли данное отправление выдать предупреждение. Если да — предупреждение печатается следом; если нет — строка просто пропускается, и список продолжается дальше. В итоге на экране появляется полный комплект этикеток для всей партии в исходном порядке поступления.

Что нужно сделать

Допишите Label, Warning, фабрику makeParcel и обработку очереди. Ввод содержит ровно три строки kind weight, где kind равен box или fragile. Для каждого значения выведите box weight=N либо fragile weight=N; сразу после хрупкой посылки выведите warning=handle-with-care. Используйте срез []Labeler и безопасную type assertion к WarningLabeler с ok.

РешениеGo

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку