· 12 мин 👁 1.1k Начинающий

Язык Go — интерфейсы

Интерфейсы в Go: как они устроены, type switch, type assertion и идиома comma-ok. С примерами из стандартной библиотеки и дополнительными сценариями.

интерфейсыtype switchtype assertionконвертацияполиморфизм
Содержание

Интерфейсы в 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