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

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

Содержание курса

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

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

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. Меняется только конкретное значение внутри.