Перейти к содержимому
Теория · Интерфейсы · урок 1 из 10

Интерфейсы

Чему вы научитесь

Чему вы научитесь:

  • объяснять, что такое интерфейс и как тип реализует его без слова implements;
  • менять то, как fmt печатает ваш тип, с помощью метода String;
  • хранить значение любого типа в any и доставать его утверждением типа;
  • выбирать действие по типу значения с помощью switch по типу.
Объявление о вакансии курьера Drive() на доске, под ним в очередь стоят студент, пенсионер и таксист

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

В программе бывает похожая ситуация. У разных типов могут быть методы с одинаковым именем, параметрами и результатом. У прямоугольника и у квадрата ниже есть метод Area, который возвращает площадь фигуры:

GO
package main

import "fmt"

type Rect struct{ Width, Height float64 }

type Square struct{ Side float64 }

func (r Rect) Area() float64   { return r.Width * r.Height }
func (s Square) Area() float64 { return s.Side * s.Side }

func main() {
    r := Rect{Width: 2, Height: 3}
    fmt.Println(r.Area(), Square{Side: 2}.Area()) // 6 4
}

Где здесь проблема? Функция, которой нужна площадь, обязана назвать тип своего параметра. Функция func printArea(r Rect) принимает только Rect, и вызов с Square не скомпилируется. Писать для каждой фигуры свою копию функции неудобно: тела у копий одинаковые, отличается только тип параметра. Нужен способ сказать «подойдёт любая фигура, которая умеет считать площадь».

Объявление интерфейса

Интерфейс — это тип, который перечисляет методы: их имена, параметры и результаты. Тел у этих методов нет. Это и есть объявление о вакансии: список того, что значение должно уметь. Объявление type Shape interface { … } состоит из частей:

  • type Shape — новый тип с именем Shape;
  • interface — вид типа: список методов;
  • Area() float64 внутри фигурных скобок — требуемый метод: имя, параметры и результат, без тела.

Интерфейс Shape требует один метод, а функция printArea принимает любое значение типа Shape:

GO
type Shape interface {
    Area() float64
}

func printArea(s Shape) {
    fmt.Println(s.Area())
}

Параметр s функции printArea имеет тип Shape. Функция знает про s одно: у него есть метод Area, и его можно вызвать. Какой тип стоит за s, функция не знает, и ей это не нужно.

Без слова implements

Тип реализует интерфейс, если у него есть все методы интерфейса с теми же параметрами и результатами. Говорят ещё, что тип удовлетворяет интерфейсу. Объявлять это не нужно: ключевого слова implements в Go нет.

Здесь аналогия с вакансией заканчивается. В жизни кандидат сам приходит и подаёт заявление, а в Go тип ничего не объявляет и даже не знает про Shape. Методы сверяет компилятор, и сверяет строго: метод Area, который возвращает int вместо float64, не подойдёт.

Соответствие компилятор проверяет в двух местах:

  • при вызове функции с параметром интерфейсного типа;
  • при присваивании значения переменной интерфейсного типа.

Если у типа нет нужного метода или его результат другого типа, программа не скомпилируется.

Разные типы в одном интерфейсе

У Rect и Square есть метод Area() float64, поэтому оба типа реализуют Shape. Функция printArea принимает любой из них, а срез []Shape может хранить их вперемешку:

GO
printArea(Rect{Width: 2, Height: 3}) // 6
printArea(Square{Side: 2})           // 4
shapes := []Shape{Square{Side: 1}, Rect{Width: 1, Height: 5}}
fmt.Println(len(shapes), shapes[1].Area()) // 2 5

Давайте разберём пример по строкам:

  1. Первый вызов передаёт в printArea прямоугольник и печатает 6.
  2. Второй передаёт квадрат и печатает 4.
  3. Срез shapes хранит две фигуры разных типов. Каждый элемент хранит значение своего конкретного типа.
  4. len(shapes) даёт 2, а shapes[1].Area() выполняет метод Area того типа, что лежит внутри, — здесь Rect с площадью 5.

Получатель-указатель и интерфейс

Этот вопрос часто задают на собеседовании: реализует ли Rect интерфейс Shape, если метод объявлен как func (r *Rect) Area() float64? Ответ зависит от вида получателя.

Метод с получателем-указателем

Нет, не реализует. Метод с получателем-указателем есть у типа *Rect, а у Rect его нет. Поэтому var s Shape = Rect{Width: 2, Height: 3} не скомпилируется: Rect does not implement Shape (method Area has pointer receiver). Подойдёт адрес: var s Shape = &Rect{Width: 2, Height: 3}.

Вызов r.Area() у переменной r при этом работает: Go сам берёт её адрес, как вы видели в прошлой теме. А для интерфейса нужен тип, у которого метод есть на самом деле.

Метод с получателем-значением

Обратное правило мягче: метод с получателем-значением есть и у Rect, и у *Rect. Поэтому с методами из нашего примера Shape реализуют оба типа.

Частая ошибка

К полю структуры обратились через переменную интерфейсного типа:

GO
var s Shape = Rect{Width: 2, Height: 3}
fmt.Println(s.Width) // ✗
TEXT
s.Width undefined (type Shape has no field or method Width)

Программа не компилируется: интерфейс перечисляет только методы, полей у него нет. Через s доступно лишь то, что объявляет Shape, — метод Area, хотя внутри s лежит Rect с полем Width. Через вакансию видно только то, что в ней написано.

Главное

  • Интерфейс перечисляет методы; тип реализует его, если у него есть все эти методы с теми же параметрами и результатами.
  • Слова implements в Go нет: соответствие проверяет компилятор.
  • Метод с получателем *Rect есть только у *Rect, поэтому интерфейс реализует *Rect, а не Rect.
  • Через переменную интерфейсного типа доступны только методы интерфейса, а не поля.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом