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

Представьте объявление о вакансии курьера. В нём не написано, кто именно нужен: студент, пенсионер или бывший таксист. Написано другое: «умеет водить машину и знает город». Любой, кто умеет эти две вещи, подходит. Работодателю неважно, кем кандидат был раньше, — важно, что он умеет.
В программе бывает похожая ситуация. У разных типов могут быть методы с одинаковым именем, параметрами и результатом. У прямоугольника и у квадрата ниже есть метод Area, который возвращает площадь фигуры:
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:
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 может хранить их вперемешку:
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Давайте разберём пример по строкам:
- Первый вызов передаёт в
printAreaпрямоугольник и печатает6. - Второй передаёт квадрат и печатает
4. - Срез
shapesхранит две фигуры разных типов. Каждый элемент хранит значение своего конкретного типа. 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 реализуют оба типа.
Частая ошибка
К полю структуры обратились через переменную интерфейсного типа:
var s Shape = Rect{Width: 2, Height: 3}
fmt.Println(s.Width) // ✗s.Width undefined (type Shape has no field or method Width)Программа не компилируется: интерфейс перечисляет только методы, полей у него нет. Через s доступно лишь то, что объявляет Shape, — метод Area, хотя внутри s лежит Rect с полем Width. Через вакансию видно только то, что в ней написано.
Главное
- Интерфейс перечисляет методы; тип реализует его, если у него есть все эти методы с теми же параметрами и результатами.
- Слова
implementsв Go нет: соответствие проверяет компилятор. - Метод с получателем
*Rectесть только у*Rect, поэтому интерфейс реализует*Rect, а неRect. - Через переменную интерфейсного типа доступны только методы интерфейса, а не поля.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.