Интерфейсы в Go: ошибки, которые портят код и ломают рефакторинг
Интерфейсы — главный инструмент абстракции в Go, но именно на них чаще всего спотыкаются: объявляют слишком рано, раздувают до десятка методов, путают указатели с значениями в реализации. Разбираем конкретные антипаттерны с кодом, объясняем, почему они вредят, и показываем, как исправить.

Зачем вообще об этом говорить
Go не даёт классов, наследования и дженериков в классическом виде — всю полиморфную работу несут интерфейсы. Но именно потому, что объявить интерфейс легко (одно ключевое слово, ни одной строки регистрации), разработчики создают их не задумываясь. Результат — кодовая база, в которой интерфейсов больше, чем реализаций, тесты зависят от моков, которые никто не читает, а рефакторинг превращается в детективную игру «кто это реализует».
Ниже — семь ошибок, которые встречаются в продакшн-коде на Go 1.21–1.23. Каждая с примером, объяснением цены и способом исправления.
Ошибка 1. Интерфейс появляется раньше второй реализации
Классический перенос привычек из Java или C#: разработчик создаёт UserRepository-интерфейс ещё до того, как у него появилась хотя бы одна альтернативная реализация. В Go принято обратное правило — accept interfaces, return structs. Интерфейс объявляется на стороне потребителя, а не на стороне провайдера.
Цена: лишний файл, лишний уровень косвенности, ложное ощущение гибкости. При рефакторинге приходится менять и интерфейс, и реализацию, хотя реализация одна.
Как исправить
Не создавайте интерфейс, пока у вас нет двух независимых потребителей с разными потребностями или пока вам не нужен мок для теста. Если мок — единственная причина, подумайте, не проще ли поднять реальную зависимость (например, тестовую базу через testcontainers).
Ошибка 2. Жирный интерфейс
Интерфейс с пятью и более методами — почти всегда признак того, что он описывает реализацию, а не контракт. В стандартной библиотеке Go самые мощные интерфейсы — маленькие: io.Reader (один метод), io.ReadWriter (два), fmt.Stringer (один).
// Плохо: потребителю нужен только поиск, но он вынужден зависеть от всего
type UserStore interface {
Create(ctx context.Context, u User) error
Update(ctx context.Context, u User) error
Delete(ctx context.Context, id string) error
FindByID(ctx context.Context, id string) (User, error)
FindByEmail(ctx context.Context, email string) (User, error)
List(ctx context.Context, filter Filter) ([]User, error)
}
// Лучше: потребитель объявляет ровно то, что использует
type UserFinder interface {
FindByID(ctx context.Context, id string) (User, error)
}
Принцип разделения интерфейсов (Interface Segregation) в Go не просто рекомендация — он встроен в дизайн языка через неявную реализацию.
Чем больше методов в интерфейсе, тем меньше типов его реализуют. Один метод — максимальная полиморфность.
Ошибка 3. Путаница pointer receiver и value receiver
Тип *T реализует все методы T, но T не реализует методы *T. Это значит, что если хотя бы один метод объявлен на указателе, передать значение в интерфейс не получится — компилятор выдаст ошибку.
type Sizer interface {
Size() int
}
type File struct {
length int
}
// Метод на указателе
func (f *File) Size() int {
return f.length
}
func report(s Sizer) {
fmt.Println(s.Size())
}
func main() {
f := File{length: 42}
// report(f) // ошибка компиляции: File does not implement Sizer
report(&f) // работает
}
Ошибка часто всплывает не при написании, а при рефакторинге: разработчик добавляет мутирующий метод на указателе, и в другом пакете ломается код, который передавал значение.
Правило большого пальца
- Если тип маленький (до 64 байт) и иммутабельный — value receiver.
- Если тип мутируется или содержит указатели, мьютексы, соединения — pointer receiver для всех методов без исключений. Смешивание ресиверов на одном типе — отдельный антипаттерн.
Ошибка 4. interface{} вместо дженериков
До Go 1.18 пустой интерфейс был единственным способом написать обобщённую функцию. С появлением дженериков (1.18+) использование interface{} или его алиаса any без ограничений — потеря типобезопасности на ровном месте.
| Подход | Типобезопасность | Читаемость | Когда уместен |
|---|---|---|---|
any / interface{} | Нет — проверка в рантайме | Низкая: type switch или reflect | Сериализация, логирование, работа с неизвестной схемой |
| Дженерик с constraint | Да — проверка при компиляции | Высокая: тип виден в сигнатуре | Коллекции, утилиты, алгоритмы |
| Конкретный интерфейс (1–2 метода) | Да — контракт по поведению | Высокая | Полиморфизм по поведению, DI |
Если вы ловите себя на type switch с тремя и более ветками — скорее всего, нужен дженерик или конкретный интерфейс, а не any.
Ошибка 5. Возврат интерфейса из конструктора
Функция NewService() ServiceInterface выглядит «чисто», но на практике вредит:
- Потребитель не видит конкретный тип, не может вызвать методы, не входящие в интерфейс.
- IDE и go vet теряют информацию о типе: автодополнение ухудшается.
- Если потребителю нужен подмножество методов, он объявит свой интерфейс — и ваш окажется лишним.
Возвращайте конкретный тип (обычно указатель на структуру). Пусть потребитель сам решает, за каким интерфейсом его спрятать.
Исключение из правила: стандартная библиотека возвращает интерфейсы там, где реализация реально скрыта и может меняться — errors.New возвращает error, context.WithCancel возвращает context.Context. Но это интерфейсы на один-два метода, определённые самим языком.
Ошибка 6. Проверка реализации только в тестах
Из-за неявной реализации интерфейсов в Go легко пропустить момент, когда тип перестал соответствовать контракту: переименовали метод, изменили сигнатуру — и узнали об этом только при запуске тестов (или в проде, если тесты не покрывают этот путь).
// Ставим compile-time проверку в файле реализации
var _ UserFinder = (*PostgresUserRepo)(nil)
Эта строка не генерирует кода, но заставляет компилятор проверить соответствие при каждой сборке. Стоит ноль в рантайме, ловит поломку мгновенно.
Ошибка 7. Nil-интерфейс и nil-указатель — не одно и то же
Одна из самых коварных ловушек Go. Интерфейсная переменная равна nil только тогда, когда у неё нет ни типа, ни значения. Если записать в интерфейс типизированный nil-указатель, проверка == nil вернёт false.
func getFile() *os.File {
return nil
}
func process(r io.Reader) {
if r == nil {
fmt.Println("nil reader")
return
}
fmt.Println("reader is not nil") // вот сюда мы попадём
}
func main() {
var f *os.File = getFile()
process(f) // r != nil, хотя внутри — nil-указатель
}
Цена в продакшне — паника при вызове метода на nil-указателе, которую вы не ожидали, потому что «проверили на nil». Решение: не передавайте типизированные nil-указатели в интерфейсы. Если функция не нашла результат, возвращайте nil интерфейсного типа явно или используйте паттерн (result, bool).
Method set (набор методов) — множество методов, определённых на типе. В Go набор методов значения T включает только методы с value receiver, а набор методов указателя *T — методы с обоими видами ресиверов. Это определяет, какие интерфейсы тип реализует.
Что делать дальше
Пройдитесь по своему проекту и посчитайте: сколько интерфейсов имеют ровно одну реализацию и при этом объявлены в пакете провайдера, а не потребителя. Если таких больше половины — начните выносить интерфейсы на сторону потребителя и удалять те, что никто не использует полиморфно. Запустите go vet ./... и включите линтер interfacebloat в golangci-lint — он предупредит об интерфейсах с числом методов выше порога (по умолчанию 10, стоит снизить до 5). Добавьте compile-time проверки var _ Interface = (*Type)(nil) для всех критичных контрактов. И перечитайте раздел «Interfaces» в Effective Go — он по-прежнему актуален для Go 1.23.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Effective Go — Interfaces (официальная документация Go), Go Wiki: CodeReviewComments — Interfaces
Частые вопросы
Когда в Go стоит создавать интерфейс?
Сколько методов допустимо в одном интерфейсе?
Почему переменная интерфейсного типа не равна nil, хотя внутри nil-указатель?
Можно ли смешивать value receiver и pointer receiver на одном типе?
Что если в проекте другая версия Go без дженериков — актуальны ли советы?
Как быстро найти «жирные» интерфейсы в существующем проекте?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.