Как в Go проверить, удовлетворяет ли структура интерфейсу?

Ответ

В Go удовлетворение интерфейсу происходит неявно (duck typing). Если структура реализует все методы интерфейса, она автоматически ему удовлетворяет. Проверить это можно статически (на этапе компиляции) или динамически (во время выполнения).

1. Статическая проверка (во время компиляции)

Это предпочтительный способ, так как он позволяет отловить ошибки до запуска программы. Для этого используется специальная идиома: присваивание nil-указателя на структуру переменной типа интерфейса. Если код компилируется, значит, структура удовлетворяет интерфейсу.

package main

type Writer interface {
    Write([]byte) (int, error)
}

type File struct{}

// File реализует метод Write, поэтому удовлетворяет интерфейсу Writer.
func (f *File) Write(data []byte) (int, error) {
    // ... логика записи
    return len(data), nil
}

// Статическая проверка: компилятор выдаст ошибку, если *File не реализует Writer.
// Используется пустой идентификатор _, чтобы переменная не была "неиспользованной".
var _ Writer = (*File)(nil)

func main() {}

2. Динамическая проверка (во время выполнения)

Этот способ используется, когда у вас есть переменная типа interface{} и нужно проверить, можно ли её привести к конкретному типу интерфейса. Для этого используется type assertion.

package main

import "fmt"

type Greeter interface {
    Greet()
}

type User struct{}

func (u User) Greet() {
    fmt.Println("Hello!")
}

func process(v interface{}) {
    // Проверяем, удовлетворяет ли значение в 'v' интерфейсу Greeter
    if greeter, ok := v.(Greeter); ok {
        fmt.Println("Тип удовлетворяет интерфейсу Greeter.")
        greeter.Greet() // Можно безопасно вызвать метод
    } else {
        fmt.Println("Тип НЕ удовлетворяет интерфейсу Greeter.")
    }
}

func main() {
    process(User{})
    process("some string")
}

Ответ 18+ 🔞

А вот смотри, как в Го эти интерфейсы работают — это ж просто песня, а не фича! Никаких явных объявлений, типа «я, мол, реализую». Если твоя структура умеет делать все методы, что интерфейс требует, то она его уже реализовала, сама того не зная. Прямо как в анекдоте: «Ты уже реализовал интерфейс! — А я и не знал!». И проверить это можно двумя путями: на этапе компиляции (статика) или уже когда программа бежит (динамика).

1. Статика — ловим косяки до запуска

Это самый правильный путь, потому что зачем тебе ждать, пока всё ебнется в рантайме, если можно всё выяснить ещё на берегу? В Го для этого есть такая хитрая штука: присваиваешь nil указатель на свою структуру переменной типа интерфейса. Если компилятор не орёт — значит, всё чисто, интерфейс реализован. Если орёт — ну, извини, дружок, где-то накосячил.

package main

type Writer interface {
    Write([]byte) (int, error)
}

type File struct{}

// File реализует метод Write, поэтому удовлетворяет интерфейсу Writer.
func (f *File) Write(data []byte) (int, error) {
    // ... логика записи
    return len(data), nil
}

// Статическая проверка: компилятор выдаст ошибку, если *File не реализует Writer.
// Используется пустой идентификатор _, чтобы переменная не была "неиспользованной".
var _ Writer = (*File)(nil)

func main() {}

Вот смотри: объявляем переменную типа Writer и пытаемся запихнуть в неё nil от *File. Компилятор тут же начинает сверять: а есть ли у *File метод Write с нужной сигнатурой? Если нет — получишь ошибку компиляции, и можешь сразу идти фиксить, а не гадать, почему в продакшене всё накрылось медным тазом. Гениально же, блядь!

2. Динамика — когда тип неизвестен до самого конца

А бывает так, что у тебя в руках переменная типа interface{} — этакая тёмная лошадка, и ты хочешь узнать, а можно ли с ней работать как с определённым интерфейсом. Вот тут на сцену выходит type assertion, или, по-нашему, «проверка на вшивость».

package main

import "fmt"

type Greeter interface {
    Greet()
}

type User struct{}

func (u User) Greet() {
    fmt.Println("Hello!")
}

func process(v interface{}) {
    // Проверяем, удовлетворяет ли значение в 'v' интерфейсу Greeter
    if greeter, ok := v.(Greeter); ok {
        fmt.Println("Тип удовлетворяет интерфейсу Greeter.")
        greeter.Greet() // Можно безопасно вызвать метод
    } else {
        fmt.Println("Тип НЕ удовлетворяет интерфейсу Greeter.")
    }
}

func main() {
    process(User{})
    process("some string")
}

Смотри, что тут происходит: функция process принимает что угодно (interface{}). А внутри мы пытаемся применить type assertion: v.(Greeter). Если v на самом деле содержит значение, которое реализует все методы Greeter, то ok будет true, и мы спокойно можем вызывать Greet(). Если нет — ну, значит, не судьба, идём по ветке else. Это как спросить: «Эй, дружок, а ты умеешь говорить „Привет“?» — и если он вдруг оказывается немым, как тот Герасим из рассказа, то… ну, ты понял.

Так что выбирай способ в зависимости от ситуации: хочешь надёжности — используй статическую проверку, а если работаешь с непредсказуемыми данными — dynamic assertion тебе в помощь. Главное — не путай их, а то будет тебе волнение ебать, а терпения-то ноль!