Интерфейсы в Go
Основная концепция
Интерфейсы в Go — это контракты, определяющие набор методов. В отличие от многих языков, интерфейсы в Go реализуются неявно (duck typing): тип автоматически удовлетворяет интерфейсу, если реализует все его методы. Не нужно явно указывать implements.
type Writer interface {
Write([]byte) (int, error)
}
// Любой тип с методом Write автоматически реализует Writer
type MyWriter struct{}
func (m MyWriter) Write(data []byte) (int, error) {
return len(data), nil
}
Структура интерфейса в памяти
Интерфейсная переменная хранит два указателя:
- itable — указатель на структуру с метаданными (тип и таблица методов)
- data — указатель на сами данные
Это важно для понимания:
interface{}(any) занимает 16 байт (2 указателя)- Сравнение интерфейсов сравнивает и тип, и значение
nilинтерфейс — это когда оба указателя nil
Пустой интерфейс interface{} / any
var x any = 42
x = "hello"
x = struct{}{}
Используется когда нужно хранить значение произвольного типа. С появлением generics (Go 1.18+) стал менее необходим, но всё ещё широко применяется.
Важные нюансы
1. nil vs nil интерфейс
Классическая ловушка:
var r *bytes.Buffer = nil
var w io.Writer = r
w == nil // false!
Потому что интерфейс хранит тип (*bytes.Buffer) и значение (nil). Тип не nil → интерфейс не nil.
2. Приемники методов (value vs pointer)
type Stringer interface {
String() string
}
type MyType struct{}
func (m MyType) String() string { return "value" } // value receiver
func (m *MyType) StringP() string { return "pointer" } // pointer receiver
var v MyType
var p *MyType
var s Stringer = v // OK
var s Stringer = &v // OK
var s Stringer = p // OK (p может быть nil)
Если метод объявлен на *Type, то интерфейсу удовлетворяет только *Type, не Type.
3. Композиция интерфейсов
type Reader interface { Read() }
type Writer interface { Write() }
type ReadWriter interface {
Reader
Writer
}
Практические паттерны
Accept interfaces, return structs
- Функции принимают интерфейсы (гибкость)
- Возвращают конкретные типы (простота, возможность добавлять методы)
Маленькие интерфейсы
- 1-3 метода — оптимально
- io.Reader, io.Writer, error — примеры
Интерфейсы для тестирования
type Storage interface {
Get(key string) (string, error)
Set(key, value string) error
}
// Легко мокать в тестах
Производительность
- Вызов метода через интерфейс немного медленнее прямого вызова (динамическая диспетчеризация)
- В большинстве приложений разница незначительна
- Для горячих путей можно использовать type assertion для прямого доступа
Type assertion и type switch
var i interface{} = "hello"
s := i.(string) // panic если не string
s, ok := i.(string) // безопасно, ok = false если не string
switch v := i.(type) {
case string:
// v - string
case int:
// v - int
default:
// неизвестный тип
}
Распространенные ошибки
- Проверка на nil через интерфейс — используйте reflect или явную проверку
- Смешивание value и pointer receivers — приводит к неожиданным ошибкам компиляции
- Слишком большие интерфейсы — нарушают принцип разделения интерфейсов
- Интерфейсы как поля структур — часто избыточно, лучше конкретный тип
Best practices
- Определяйте интерфейсы там, где они используются (consumer-side), а не там, где реализуются
- Не создавайте интерфейсы "на вырост" — добавляйте когда появится реальная потребность
- Используйте
anyвместоinterface{}(Go 1.18+) - Для ошибок используйте интерфейс
error, а не конкретные типы