Язык Go — интерфейсы
Интерфейсы в Go: как они устроены, type switch, type assertion и идиома comma-ok. С примерами из стандартной библиотеки и дополнительными сценариями.
Содержание
Интерфейсы в Go описывают поведение: если тип умеет делать что-то, он может использоваться там, где это что-то требуется. Это главный механизм полиморфизма в Go — без наследования и без явной декларации «реализую интерфейс X».
Что такое интерфейс в Go
Интерфейс — это набор сигнатур методов. Любой тип, у которого есть все эти методы, автоматически реализует интерфейс. Явно писать implements не нужно.
Интерфейсы с одним-двумя методами — норма в Go. Имя обычно производится от метода:
// io.Writer — интерфейс из стандартной библиотеки
// реализует любой тип, у которого есть метод Write
type Writer interface {
Write(p []byte) (n int, err error)
}
// fmt.Stringer — интерфейс для кастомного вывода
// реализует любой тип, у которого есть метод String
type Stringer interface {
String() string
}
fmt.Fprintf принимает io.Writer — значит, можно передать файл, буфер, сетевое соединение, или свой тип, лишь бы у него был метод Write.
Тип может реализовывать несколько интерфейсов
Один тип может удовлетворять сразу нескольким интерфейсам. Рассмотрим тип Sequence — слайс целых чисел, который умеет и сортироваться, и красиво выводиться:
type Sequence []int
// --- sort.Interface: три метода для сортировки ---
func (s Sequence) Len() int {
return len(s)
}
func (s Sequence) Less(i, j int) bool {
return s[i] < s[j] // сравниваем элементы
}
func (s Sequence) Swap(i, j int) {
s[i], s[j] = s[j], s[i] // меняем местами
}
// Copy возвращает копию, чтобы не менять оригинал
func (s Sequence) Copy() Sequence {
copy := make(Sequence, 0, len(s))
return append(copy, s...)
}
// --- fmt.Stringer: метод String для красивого вывода ---
func (s Sequence) String() string {
s = s.Copy() // работаем с копией, не трогаем оригинал
sort.Sort(s) // сортируем перед выводом
str := "["
for i, elem := range s {
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}
Sequence реализует sort.Interface (три метода: Len, Less, Swap) и fmt.Stringer (метод String). Никакого объявления об этом не нужно — Go проверяет соответствие автоматически.
Конвертация типов как способ переиспользовать методы
Метод String выше воссоздаёт то, что fmt.Sprint уже умеет делать со слайсами — и делает это за O(N²). Можно проще: сконвертировать Sequence в []int и вызвать Sprint напрямую:
func (s Sequence) String() string {
s = s.Copy()
sort.Sort(s)
return fmt.Sprint([]int(s)) // конвертируем в []int — Sprint уже знает как его вывести
}
Конвертация []int(s) не создаёт новый массив. Sequence и []int — один и тот же тип данных, разные только имена. Конвертация лишь говорит компилятору «смотри на это как на []int», чтобы получить доступ к другому набору методов.
Можно пойти ещё дальше и использовать sort.IntSlice:
func (s Sequence) String() string {
s = s.Copy()
sort.IntSlice(s).Sort() // конвертируем в sort.IntSlice и вызываем его метод Sort
return fmt.Sprint([]int(s))
}
Теперь Sequence не реализует sort.Interface сам — вместо этого мы конвертируем его в тип, который уже умеет сортироваться. Реализация четырёх методов сжалась до одной строки.
Type switch: разные действия для разных типов
type switch — это конструкция, которая проверяет динамический тип интерфейсного значения и выполняет разные ветки для разных типов. Вот упрощённая версия того, как fmt.Printf превращает значение в строку:
type Stringer interface {
String() string
}
func stringify(value interface{}) string {
switch str := value.(type) {
case string:
// str здесь имеет тип string — извлекаем конкретное значение
return str
case Stringer:
// str здесь имеет тип Stringer — вызываем метод
return str.String()
default:
return fmt.Sprintf("%v", value)
}
}
В первом case из интерфейса извлекается конкретное значение типа string. Во втором — интерфейс конвертируется в другой интерфейс Stringer. Это разные операции, но синтаксис одинаковый.
Дополнительный пример: обработка разных типов событий
type ClickEvent struct{ X, Y int }
type KeyEvent struct{ Key string }
type ScrollEvent struct{ Delta int }
func handleEvent(e interface{}) {
switch ev := e.(type) {
case ClickEvent:
fmt.Printf("клик в точке (%d, %d)\n", ev.X, ev.Y)
case KeyEvent:
fmt.Printf("нажата клавиша: %s\n", ev.Key)
case ScrollEvent:
fmt.Printf("прокрутка на %d\n", ev.Delta)
default:
fmt.Printf("неизвестное событие: %T\n", ev) // %T печатает тип
}
}
handleEvent(ClickEvent{X: 10, Y: 20}) // клик в точке (10, 20)
handleEvent(KeyEvent{Key: "Enter"}) // нажата клавиша: Enter
handleEvent(ScrollEvent{Delta: -3}) // прокрутка на -3
Type assertion: извлечение конкретного типа
Если нас интересует только один конкретный тип — не нужен целый type switch. Достаточно type assertion:
var value interface{} = "hello"
// value.(string) — утверждаем, что внутри interface{} лежит string
str := value.(string)
fmt.Println(str) // hello
Но если внутри окажется не string — программа упадёт с паникой:
var value interface{} = 42
str := value.(string) // panic: interface conversion: interface {} is int, not string
Идиома comma-ok: безопасная проверка
Чтобы не словить панику, используют двойное присвоение — comma-ok:
var value interface{} = 42
str, ok := value.(string) // ok == false, str == "" (нулевое значение для string)
if ok {
fmt.Printf("строка: %q\n", str)
} else {
fmt.Printf("не строка, а %T\n", value) // не строка, а int
}
Если ok == false, переменная str всё равно существует — она получает нулевое значение для типа string, то есть пустую строку "". Паники нет.
Эквивалентная запись через if вместо type switch:
if str, ok := value.(string); ok {
return str
} else if stringer, ok := value.(Stringer); ok {
return stringer.String()
}
Дополнительный пример: middleware с проверкой расширенного интерфейса
Паттерн встречается в стандартной библиотеке: http.ResponseWriter — базовый интерфейс, но некоторые реализации дополнительно поддерживают http.Flusher. Проверяем это через type assertion:
type Flusher interface {
Flush()
}
func streamResponse(w http.ResponseWriter) {
// проверяем: поддерживает ли конкретная реализация Flush?
flusher, ok := w.(Flusher)
if !ok {
// не поддерживает — просто пишем всё сразу
fmt.Fprintln(w, "данные без стриминга")
return
}
// поддерживает — отправляем данные порциями
for i := 0; i < 5; i++ {
fmt.Fprintf(w, "порция %d\n", i)
flusher.Flush() // сбрасываем буфер клиенту немедленно
time.Sleep(500 * time.Millisecond)
}
}
Такой подход позволяет писать код, который работает с минимальным интерфейсом, но использует расширенные возможности там, где они есть.
Итоги
- Интерфейс — набор сигнатур методов; тип реализует его автоматически, если у него есть все нужные методы
- Один тип может реализовывать несколько интерфейсов одновременно
- Конвертация типов позволяет переиспользовать методы другого типа без дублирования кода
type switch— проверяет динамический тип интерфейсного значения и выполняет разные веткиvalue.(Type)—type assertion, извлекает конкретный тип; паникует, если тип не совпадаетv, ok := value.(Type)— безопасная форма через идиомуcomma-ok; при неудачеok == false, паники нет- Паттерн «проверить расширенный интерфейс через
type assertion» — стандартная практика в Go