Каковы лучшие практики при работе с интерфейсами в Go?

Ответ

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

// Определяем интерфейс с одним методом
type Reader interface {
    Read(p []byte) (n int, err error)
}

Лучшие практики и идиомы Go:

  1. Небольшие и сфокусированные интерфейсы: Предпочитайте маленькие интерфейсы (1-3 метода). Это соответствует принципу разделения интерфейсов (Interface Segregation Principle). io.Reader, io.Writer, fmt.Stringer — идеальные примеры.

  2. Именование: Для интерфейсов с одним методом принято добавлять суффикс -er к названию метода (Read -> Reader, Write -> Writer).

  3. "Принимай интерфейсы, возвращай структуры": Это ключевая идиома Go. Ваши функции должны зависеть от абстракций (интерфейсов), а не от конкретных типов. Это делает код более тестируемым и гибким. Возвращать же лучше конкретный тип, чтобы у вызывающего кода был доступ ко всей его функциональности.

  4. Объявление на стороне потребителя: Интерфейс должен определять тот пакет, который его использует, а не тот, который его реализует. Это позволяет избежать циклических зависимостей и лишних связей между пакетами.

  5. Неявная реализация: В Go тип удовлетворяет интерфейсу автоматически, если он реализует все его методы. Ключевое слово implements не требуется. Это способствует слабой связности.

  6. Пустой интерфейс (any): interface{} (или его псевдоним any с Go 1.18) может содержать значение любого типа. Используйте его, когда тип данных действительно неизвестен. Для работы с таким значением требуются проверки типа (type assertions).

Пример использования:

// Интерфейс определяет, что нам нужно что-то, что умеет логировать
type Logger interface {
    Log(string)
}

// Конкретная реализация
type ConsoleLogger struct{}

func (c ConsoleLogger) Log(msg string) {
    fmt.Println(msg)
}

// Функция зависит от интерфейса, а не от ConsoleLogger
func Process(l Logger) {
    l.Log("Процесс запущен...")
}

func main() {
    // Мы можем передать любую структуру, которая удовлетворяет интерфейсу Logger
    cl := ConsoleLogger{}
    Process(cl)
}

Ответ 18+ 🔞

А, интерфейсы в Go, ёпта! Ну это ж, блядь, как договор, понимаешь? Не про то, какие у тебя поля в структуре, а про то, что ты, сука, умеешь делать. Чистая абстракция, в рот меня чих-пых!

Смотри, вот тебе самый простой пример, как они выглядят:

// Объявляем, что нам нужен кто-то, кто умеет читать. И всё.
type Reader interface {
    Read(p []byte) (n int, err error)
}

Вот и весь интерфейс. Говорит: «Мужик, если у тебя есть метод Read с такой вот сигнатурой — поздравляю, ты теперь `Reader». И не надо никаких заявлений «я имплементирую», сам догадается, хитрая жопа.

А теперь, слушай сюда, как этим не обосраться. Правила, выстраданные кровью и дебагом:

  1. Делай интерфейсы маленькими, как совесть у политика. Один-два метода — идеально. Не надо лепить в один интерфейс ReadWriteCloseParseSerialize. Это же не новогодний набор, блядь. Посмотри на стандартную библиотеку — io.Reader, io.Writer. Гении, ёпта. Одна работа.

  2. Как назвать? Да похуй, но если метод один — просто возьми его имя и прилепи -er. Read -> Reader, String -> Stringer. Всё гениальное просто, как хуй с горы.

  3. Главная мантра, которую надо повторять перед сном: «Принимай интерфейсы, возвращай структуры». Это что за хуйня? А вот что: твои функции должны хотеть не конкретную структуру SuperMegaDatabase, а просто кого-то, кто умеет Query(). А возвращать ты должен конкретную, живую, потрогать можно структуру. Так и тестировать проще (подсунул заглушку), и переиспользовать. Мозги включай!

  4. Кто главный? Тот, кто пользуется! Интерфейс объявляет тот, кому он нужен для работы, а не тот, кто его реализует. Ты же не идешь на завод и не говоришь: «Слушай, я тут умею дышать и есть, напиши мне на лбу „Человек“». Нет. Заводу нужен работник, он говорит: «Мне нужен кто-то, кто умеет Сверлить()». И если ты умеешь — ты подходишь. Вот и всё. Так зависимости не путаются.

  5. Никаких implements! В Go тип просто берёт и удовлетворяет интерфейсу, если методы подходят. Молча. Как тот самый Герасим. «Муму» — и всё, ты в клубе. Слабая связность, ебать её в сраку, красота.

  6. Пустой интерфейс interface{} (или any). Это, блядь, чёрная дыра, всеядная жопа. Может принять что угодно. Используй, только когда реально не знаешь, что придёт. А потом доставай оттуда значение через type assertion и молись, чтобы не было panic. «Ой, а я думал, тут int, а тут, сука, chan struct{}...» — пиздец.

Ну и смотри, как это в жизни выглядит:

// Нам нужен просто кто-то, кто базарит в консоль. Логгер, короче.
type Logger interface {
    Log(string)
}

// Конкретный чувак, который это делает.
type ConsoleLogger struct{}

func (c ConsoleLogger) Log(msg string) {
    fmt.Println(msg)
}

// Наша функция. Ей похуй, кто именно логирует. Лишь бы метод Log был.
func Process(l Logger) {
    l.Log("Процесс запущен...")
}

func main() {
    // Создаём конкретного чувака...
    cl := ConsoleLogger{}
    // ...и суём его в функцию. Она его примет, потому что он подходит под описание.
    Process(cl)
}

Вот и вся магия. Никакой ебли с наследованием, только контракты, только хардкор. Сначала мозг ломает, а потом понимаешь — а ведь это охуенно просто и мощно. Главное — не начинай с больших интерфейсов, а то получится пиздопроебибна, которую потом не перепишешь.