Урок курса

Интерфейсы: определение, неявная реализация и 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

Продолжить с проверкой и прогрессом

Откройте интерактивный раннер с заданиями урока.

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