Interfaces в Go простыми словами

Интерфейсы в Go часто объясняют слишком рано и слишком абстрактно. Поэтому начнем не с терминов, а с задачи: у нас есть сервис задач, которому нужно сохранять текст. Сегодня он сохраняет задачи в память, завтра — в PostgreSQL, послезавтра — в файл. Логика сервиса не должна знать, куда именно ушли данные

Для этого и пригодится interface: он описывает поведение, а не конкретный тип

Что получится в конце

Мы напишем маленькую программу:

type Storage interface {
    Save(task string) error
}

И тип, который подходит этому интерфейсу без слова implements:

type MemoryStorage struct {
    tasks []string
}

func (s *MemoryStorage) Save(task string) error {
    s.tasks = append(s.tasks, task)
    return nil
}

Главный эффект: Go сам понимает, что *MemoryStorage реализует Storage, потому что у него есть нужный метод

Создаем проект

mkdir go-interfaces
cd go-interfaces
go mod init example.com/go-interfaces

Создайте main.go:

package main

import "fmt"

type Storage interface {
    Save(task string) error
}

type MemoryStorage struct {
    tasks []string
}

func (s *MemoryStorage) Save(task string) error {
    s.tasks = append(s.tasks, task)
    return nil
}

type TaskService struct {
    storage Storage
}

func (s TaskService) Add(task string) error {
    if task == "" {
        return fmt.Errorf("task is empty")
    }

    return s.storage.Save(task)
}

func main() {
    storage := &MemoryStorage{}
    service := TaskService{storage: storage}

    if err := service.Add("Разобрать interfaces в Go"); err != nil {
        fmt.Println("Ошибка:", err)
        return
    }

    fmt.Println("Задачи:", storage.tasks)
}

Запустите:

go run .

Вывод:

Задачи: [Разобрать interfaces в Go]

Что именно описывает interface

Интерфейс:

type Storage interface {
    Save(task string) error
}

говорит: нам нужен любой тип, у которого есть метод Save, принимающий строку и возвращающий error. Интерфейс не говорит, как именно сохранять задачу. Он описывает только контракт

Это важное отличие от struct. struct отвечает на вопрос «какие данные храним». interface отвечает на вопрос «что этот тип умеет делать»

Почему нет implements

В Go не пишут:

type MemoryStorage implements Storage

Такой записи нет. Тип подходит интерфейсу автоматически, если у него есть все методы интерфейса

В нашем примере метод:

func (s *MemoryStorage) Save(task string) error

совпадает с требованием:

Save(task string) error

Значит *MemoryStorage можно передать туда, где ожидается Storage

Это называется implicit implementation. Сначала звучит странно, но потом оказывается удобным: типы не привязаны к интерфейсам заранее, а интерфейсы можно создавать рядом с тем кодом, которому они нужны

Зачем TaskService принимает Storage

Структура:

type TaskService struct {
    storage Storage
}

не знает про MemoryStorage. Она знает только, что у хранилища есть метод Save. Благодаря этому позже можно сделать другое хранилище:

type ConsoleStorage struct{}

func (s ConsoleStorage) Save(task string) error {
    fmt.Println("Сохраняю:", task)
    return nil
}

И подставить его:

service := TaskService{storage: ConsoleStorage{}}

Код TaskService.Add менять не нужно. Он работает с поведением, а не с конкретной реализацией

Где интерфейс лучше не нужен

Интерфейс не стоит добавлять «на всякий случай». Если у вас один тип, одна функция и нет реальной замены реализации, обычный struct будет проще

Плохой сигнал:

type UserServiceInterface interface {
    CreateUser(name string) error
}

если рядом есть только один UserService, и никто больше этот контракт не использует. Такой интерфейс не делает код гибче, он просто добавляет слой, через который нужно читать

Практическое правило для новичка: сначала пишите конкретный тип. Интерфейс добавляйте тогда, когда появился второй вариант поведения, тестовая заглушка или функция, которой действительно все равно, какая реализация внутри

Маленькая проверка на ошибку

Измените метод в MemoryStorage:

func (s *MemoryStorage) Save(task string) {
    s.tasks = append(s.tasks, task)
}

Теперь метод не возвращает error. Запустите:

go run .

Компилятор скажет, что *MemoryStorage не реализует Storage, потому что сигнатура метода не совпадает. Это хороший тип ошибок: Go не просто падает, а объясняет, какого метода или какой формы метода не хватает

Верните error обратно, чтобы программа снова работала

Частые ошибки

Тип вроде бы реализует интерфейс, но компилятор не согласен. Проверьте сигнатуру метода: имя, параметры, возвращаемые значения и receiver должны совпасть по смыслу

*Метод есть у Type, но не у Type.* Если метод объявлен с pointer receiver, интерфейс реализует указатель на тип, например MemoryStorage, а не всегда само значение MemoryStorage

Интерфейс слишком большой. В Go любят маленькие интерфейсы. Один-два метода часто лучше, чем общий интерфейс на десять действий

Интерфейс создан раньше потребности. Это частая привычка после языков с классами. В Go интерфейсы обычно рождаются рядом с потребителем, а не заранее рядом с реализацией

Что может быть еще интересно по этой теме

Почему interfaces в Go считаются сильной стороной языка? Потому что они дают слабую связность без тяжелой иерархии. Типу не нужно заранее объявлять, какие контракты он поддерживает

Можно ли сделать пустой interface? Можно, но в современном Go чаще пишут any, который является псевдонимом для interface{}. Новичку не стоит использовать его без нужды: вы теряете информацию о типе

Где интерфейсы пригодятся в реальном backend? В логгерах, хранилищах, отправке писем, клиентах внешних API, тестовых заглушках и местах, где бизнес-логика не должна зависеть от конкретной инфраструктуры

Что лучше прочитать перед интерфейсами? Уверенно понять struct и методы. Без receiver интерфейсы выглядят как туман, а после методов становятся почти очевидными

Что открыть дальше

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x