Интерфейс описывает, что объект умеет делать, не говоря, как он устроен внутри. В Go это сделано необычно: тип реализует интерфейс автоматически, без единого слова об этом в коде. Разберём, как это работает и почему такой подход меняет привычки.
Зачем нужны интерфейсы
Представь функцию, которая что-то логирует. Сегодня она пишет в консоль, завтра — в файл, послезавтра — в сетевой сервис. Если функция жёстко завязана на конкретный тип, каждый новый получатель — переписывание. Если она принимает интерфейс «что-то, во что можно писать», то любой подходящий тип подойдёт без изменений в самой функции.
Короткая формула: интерфейс — это набор методов, которые тип обязан иметь. Кто эти методы имеет — тот и подходит.
type Writer interface {
Write(data []byte) (int, error)
}
Любой тип с методом Write(data []byte) (int, error) считается Writer. Файл, буфер в памяти, сетевое соединение — все подходят под один контракт.
Неявная реализация (duck typing на этапе компиляции)
В большинстве языков нужно прямо написать «этот класс реализует этот интерфейс» (implements, : IWriter и так далее). В Go этого нет. Если у типа есть все методы интерфейса — он его реализует. Автоматически.
type Greeter interface {
Greet() string
}
type Russian struct{}
// Метод Greet делает Russian пригодным как Greeter — без объявлений.
func (r Russian) Greet() string {
return "Привет"
}
func sayHello(g Greeter) {
println(g.Greet())
}
func main() {
sayHello(Russian{}) // работает: у Russian есть метод Greet
}
Это называют duck typing («если крякает как утка — значит, утка»), но с важной оговоркой: проверка происходит на этапе компиляции, а не в момент запуска. Забыл метод — программа просто не соберётся, а не упадёт у пользователя.
Плюс такого подхода: интерфейс можно объявить там, где он нужен (в пакете-потребителе), а тип, который под него подходит, — где угодно, даже в чужой библиотеке, которую ты не можешь править. Тип и контракт не обязаны знать друг о друге.
Если хочешь явно убедиться, что тип реализует интерфейс, есть идиома проверки на этапе компиляции:
var _ Greeter = Russian{} // не соберётся, если Russian перестанет подходить
Маленькие интерфейсы
Главная привычка в Go — держать интерфейсы маленькими. Чем меньше методов, тем больше типов под него подходит и тем проще его реализовать. Идеал — один метод.
В стандартной библиотеке это видно невооружённым глазом: io.Reader и io.Writer — по одному методу каждый, и на них держится вся работа с потоками данных.
type Reader interface {
Read(p []byte) (n int, err error)
}
Один метод — а под него подходят файлы, сетевые соединения, архивы, буферы. Функция, принимающая io.Reader, работает со всеми сразу.
Короткая формула: много маленьких интерфейсов лучше, чем один большой. Большой интерфейс с десятком методов трудно реализовать и неудобно подменять в тестах.
Пустой интерфейс: any
Интерфейс без методов подходит под любой тип — ведь требований ноль. Раньше его писали как interface{}, с Go 1.18 есть понятный синоним — any.
func describe(v any) {
println(v) // сюда можно передать что угодно
}
describe(42)
describe("текст")
describe([]int{1, 2, 3})
any удобен, но это отказ от типобезопасности: компилятор больше не знает, что внутри. Чтобы что-то сделать со значением, придётся доставать его конкретный тип обратно (об этом ниже). Поэтому any — крайнее средство: для по-настоящему разнородных данных (например, разбор JSON неизвестной структуры), а не замена нормальным типам.
Утверждение типа (type assertion)
Когда значение лежит в интерфейсе (особенно в any), достать из него конкретный тип помогает утверждение типа: v.(T).
var v any = "привет"
s := v.(string) // достаём строку
println(s) // привет
Если тип не совпал, такая форма вызовет панику и уронит программу. Поэтому почти всегда используют безопасную форму с двумя значениями — второе говорит, удалось ли:
var v any = 42
s, ok := v.(string)
if !ok {
println("это не строка") // ok == false, паники нет
}
Короткая формула: x, ok := v.(T) — всегда, голую v.(T) — только когда уверен на 100%.
type switch
Когда вариантов несколько, проверять по одному утомительно. Для этого есть type switch — switch по типу значения:
func describe(v any) string {
switch x := v.(type) {
case int:
return "целое число"
case string:
return "строка длиной " + itoa(len(x)) // x здесь уже string
case bool:
return "логическое значение"
default:
return "неизвестный тип"
}
}
В каждой ветке переменная x уже имеет нужный конкретный тип — приводить ничего не надо. Это основной инструмент, когда в any может прийти несколько разных типов.
«Принимай интерфейсы, возвращай структуры»
Распространённый совет в Go: функция принимает интерфейс, а возвращает конкретный тип (структуру).
Зачем принимать интерфейс: функция требует от вызывающего только то, что реально использует. Передать можно любой подходящий тип, в том числе подделку в тестах. Гибкость на входе.
// Хорошо: нужен только Read — просим минимум.
func countBytes(r io.Reader) (int, error) { /* ... */ }
Зачем возвращать структуру: вызывающий получает полноценный конкретный тип со всеми полями и методами, ничего не теряя. Если бы функция вернула интерфейс, она бы заранее урезала то, что доступно вызывающему.
// Хорошо: вернули конкретный *bytes.Buffer, у вызывающего все его возможности.
func newBuffer() *bytes.Buffer { /* ... */ }
Ещё одно следствие: не вводи интерфейс заранее, «на всякий случай». В Go его можно объявить в любой момент, когда реально появится второй вариант реализации. Поэтому интерфейсы добавляют по факту нужды, а не авансом.
Связь с backend-Go
Здесь мы говорим про интерфейсы как возможность языка. Как они применяются в реальной серверной разработке — подмена зависимостей в HTTP-обработчиках, port-интерфейсы для базы данных, мок-реализации в тестах — это раздел /go/. Там те же интерфейсы работают как швы между слоями приложения.
Коротко
- Интерфейс — набор методов; тип подходит, если имеет их все.
- Реализация неявная: никаких
implements, проверка на этапе компиляции (duck typing, но безопасный). - Держи интерфейсы маленькими — идеал один метод (
io.Reader,io.Writer); много мелких лучше одного большого. any(бывшийinterface{}) подходит под всё, но отключает типобезопасность — крайнее средство.- Достать конкретный тип: утверждение типа
x, ok := v.(T)(безопасная форма) или type switch для нескольких вариантов. - Принимай интерфейсы, возвращай структуры; интерфейс вводи по факту нужды, а не заранее.
Что почитать дальше
- Структуры и методы — как объявляются типы и методы, на которых держатся интерфейсы.
- Ошибки в Go —
errorэто интерфейс; type switch и утверждение типа постоянно встречаются при работе с ошибками. - Слайсы и map — базовые структуры данных, которые часто прячут за интерфейсами.