Урок курса
Интерфейсы: определение, неявная реализация и 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 оправдан там, где тип значения принципиально неизвестен на этапе написания кода — например, в контейнерах разнотипных элементов или в функциях-обёртках. Если ты знаешь, какие типы будут использоваться, лучше объявить конкретный интерфейс с нужными методами — тогда компилятор работает на твоей стороне.
Попробуйте решить
Тип 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
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
