Что означает идиома «Принимай интерфейсы, возвращай структуры» в Go?

Ответ

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

1. Принимай интерфейсы (Accept Interfaces)

Функции и методы должны принимать в качестве аргументов интерфейсы, а не конкретные типы.

  • Почему? Это делает функцию более универсальной и тестируемой. Она зависит от поведения (методов интерфейса), а не от конкретной реализации. Вы можете передать в неё любой тип, который удовлетворяет этому интерфейсу, включая моки для тестов.
// Функция принимает любой тип, реализующий io.Reader
func countLines(r io.Reader) (int, error) {
    scanner := bufio.NewScanner(r)
    count := 0
    for scanner.Scan() {
        count++
    }
    return count, scanner.Err()
}

// Мы можем передать как файл, так и строку:
file, _ := os.Open("text.txt")
countLines(file)

strReader := strings.NewReader("line1nline2")
countLines(strReader)

2. Возвращай структуры (Return Structs)

Функции, которые создают и возвращают новые значения, должны возвращать конкретные типы (чаще всего указатели на структуры), а не интерфейсы.

  • Почему? Это даёт потребителю (вызывающему коду) максимальную гибкость. Возвращая конкретный тип, вы предоставляете всю его функциональность (все поля и методы). Потребитель сам решит, нужно ли ему использовать это значение через какой-либо интерфейс или работать с ним напрямую. Если бы вы вернули интерфейс, потребитель был бы ограничен только методами этого интерфейса.
// Фабричная функция возвращает конкретный тип *User
type User struct {
    ID   int
    Name string
}

func NewUser(name string) *User {
    return &User{Name: name}
}

// Потребитель получает *User и может делать с ним что угодно
user := NewUser("Alice")
fmt.Println(user.ID, user.Name)

Итог: Этот принцип приводит к созданию API, которые легко использовать и расширять, при этом сохраняя низкую связанность между компонентами системы.