Интерфейсы - контракты в Go

Интерфейсы - контракты в Go

Интерфейсы в Go - это то, что делает язык по-настоящему гибким. Они определяют контракт (набор методов), но не реализацию. И самое крутое - реализация неявная!

Основы интерфейсов

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

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

type Closer interface {
    Close() error
}

// Композиция интерфейсов
type WriteCloser interface {
    Writer
    Closer
}

Неявная реализация

type File struct {
    name string
}

// File автоматически реализует Writer
func (f *File) Write(data []byte) (int, error) {
    fmt.Printf("Writing to %s: %s\n", f.name, string(data))
    return len(data), nil
}

// Использование
var w Writer = &File{name: "test.txt"}
w.Write([]byte("Hello, Go!"))

Что лежит внутри интерфейсного значения

Когда вы пишете var w io.Writer = file, переменная w - это пара указателей: на таблицу типа с методами (itab) и на сами данные. Поэтому интерфейс всегда занимает 16 байт на 64-битной платформе, а вызов w.Write(...) идёт через косвенный диспатч.

Интерфейс это два указателя: на itab с методами и на данные

itab кэшируется в runtime: для каждой пары (тип, интерфейс) создаётся одна таблица и переиспользуется.

Утиная типизация (duck typing)

"If it walks like a duck and quacks like a duck, it's a duck."

Тип «является» интерфейсом, если у него есть нужные методы - без явных объявлений вроде implements и без наследования. Формально это структурная типизация (проверку делает компилятор), но идея та же, что у динамических языков: важно поведение, а не иерархия.

Главное практическое следствие: интерфейс описывает потребитель, а не поставщик. Можно объявить свой интерфейс уже после того, как чужой тип написан - и он автоматически подойдёт, если сигнатуры совпали.

Если метод объявлен на указателе, интерфейс реализует только указатель - но не значение.
func (u *User) String() string { return u.Name }

var s fmt.Stringer
s = &User{Name: "Иван"} // OK
s = User{Name: "Иван"}  // compile error: метод есть только у *User

Пустой интерфейс

// interface{} или any (Go 1.18+)
var i interface{} = 42
i = "hello"
i = []int{1, 2, 3}

// Функция, принимающая что угодно
func Print(v any) {
    fmt.Println(v)
}

Type assertion и type switch

Type assertion

var i interface{} = "hello"

// Безопасное преобразование
s, ok := i.(string)
if ok {
    fmt.Println("Строка:", s)
}

// Паника при неудаче
s2 := i.(string) // OK
n := i.(int)     // panic!

Type switch

Подробнее про обычный switch и type switch - в уроке if, switch и логика решений.

func describe(i interface{}) {
    switch v := i.(type) {
    case string:
        fmt.Printf("Строка длиной %d: %s\n", len(v), v)
    case int:
        fmt.Printf("Число: %d\n", v)
    case bool:
        fmt.Printf("Булево: %t\n", v)
    default:
        fmt.Printf("Неизвестный тип: %T\n", v)
    }
}

Стандартные интерфейсы

io.Reader и io.Writer

type Reader interface {
    Read([]byte) (int, error)
}

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

// Пример реализации
type UpperWriter struct {
    w io.Writer
}

func (u *UpperWriter) Write(data []byte) (int, error) {
    return u.w.Write(bytes.ToUpper(data))
}

// Композиция
upper := &UpperWriter{w: os.Stdout}
fmt.Fprintln(upper, "hello world") // HELLO WORLD

fmt.Stringer

type Stringer interface {
    String() string
}

type Point struct {
    X, Y int
}

func (p Point) String() string {
    return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}

// Автоматически используется в fmt
p := Point{3, 4}
fmt.Println(p) // (3, 4)

error

type error interface {
    Error() string
}

// Собственный тип ошибки
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}

Проектирование интерфейсов

Маленькие интерфейсы

// Хорошо: маленькие, сфокусированные интерфейсы
type Reader interface {
    Read([]byte) (int, error)
}

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

// Плохо: большой интерфейс
type FileSystem interface {
    Open(string) (File, error)
    Create(string) (File, error)
    Remove(string) error
    Rename(string, string) error
    Stat(string) (FileInfo, error)
    // ... еще 10 методов
}
"The bigger the interface, the weaker the abstraction" - Rob Pike

Композиция интерфейсов

type ReadWriter interface {
    Reader
    Writer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

type ReadWriteSeeker interface {
    Reader
    Writer
    Seeker
}

Где интерфейсы работают на практике

Интерфейсы - фундамент для большинства архитектурных приёмов и паттернов проектирования. Здесь важно понять, как они устроены; куда их применять - разбираем в отдельных треках:

  • Strategy - взаимозаменяемые алгоритмы за единым контрактом (оплата картой/через PayPal/криптой). Подробно в уроке GoF · Strategy.
  • Decorator - добавление поведения без изменения исходного типа (логирование, кеш, ретраи поверх существующего сервиса). См. GoF · Decorator.
  • Adapter - приведение чужого API к нашему интерфейсу. См. GoF · Adapter.
  • Dependency Injection - сервис принимает зависимости через интерфейсы, а не создаёт конкретные типы внутри себя. Это даёт тестируемость и слабую связанность. См. Hex Architecture · Ports & Adapters и Hex Architecture · Testing.
  • Mock-объекты в тестах - подменяем реальную зависимость заглушкой, реализующей тот же интерфейс. Пример смотри в секции «Testing с интерфейсами» ниже.

Каноничный пример из стандартной библиотеки - HTTP middleware на базе http.Handler - разобран отдельной секцией ниже: он полезен сам по себе, потому что встретится в треке про HTTP.

Nil интерфейсы

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

var w Writer // w == nil, тип и значение nil

type myWriter struct{}
func (m *myWriter) Write(p []byte) (int, error) {
    return len(p), nil
}

var mw *myWriter // mw == nil
w = mw           // w != nil, но значение внутри nil!

if w != nil {
    w.Write([]byte("test")) // panic если метод не проверяет receiver
}
Интерфейс равен nil только если и тип, и значение nil. Интерфейс с nil указателем конкретного типа != nil.

Практический пример: HTTP middleware

type Middleware func(http.Handler) http.Handler

func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

func Authentication(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Композиция
handler := http.HandlerFunc(myHandler)
handler = Logging(Authentication(handler))

Testing с интерфейсами

type Database interface {
    Get(id string) (string, error)
    Set(id, value string) error
}

type MockDatabase struct {
    data map[string]string
}

func (m *MockDatabase) Get(id string) (string, error) {
    if val, ok := m.data[id]; ok {
        return val, nil
    }
    return "", errors.New("not found")
}

func (m *MockDatabase) Set(id, value string) error {
    m.data[id] = value
    return nil
}

// В тестах используем mock
func TestService(t *testing.T) {
    db := &MockDatabase{data: make(map[string]string)}
    service := NewService(db)
    // тестируем...
}

Best practices

<ComparisonTable data={{ headers: ["Практика", "Плохо", "Хорошо"], rows: [ ["Размер интерфейса", "type Storage interface { /* 15 методов */ }", "type Reader interface { Read([]byte) (int, error) }"], ["Именование", "type WriterInterface interface {}", "type Writer interface {}"], ["Проверка реализации", "// никакой проверки", "var _ Writer = (*MyWriter)(nil)"], ["Возврат интерфейсов", "func New() *ConcreteType", "func New() Interface // если нужна гибкость"] ] }} />

Итоги

  • Интерфейсы определяют поведение, не реализацию
  • Реализация неявная (duck typing)
  • Маленькие интерфейсы - хорошие интерфейсы
  • interface{} для универсальности
  • Мощный инструмент для тестирования

В следующем уроке разберем обработку ошибок - философию Go!

Типичная ошибка

Делать «универсальный интерфейс на всё». Это как швейцарский нож... но без ножа.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.