Интерфейсы
1. Что такое интерфейс?
В Go интерфейс — это контракт, который описывает поведение объекта. Он содержит набор сигнатур методов, но не содержит их реализации.
Главная фишка Go — Duck Typing (Утиная типизация).
В Go нет ключевого слова implements (как в Java или PHP). Тип реализует интерфейс неявно. Если структура имеет все методы, перечисленные в интерфейсе, значит, она автоматически реализует этот интерфейс. "Если птица ходит как утка и крякает как утка, то это утка."
type Speaker interface {
Speak() string
}
type Dog struct{}
// Dog неявно реализует интерфейс Speaker
func (d Dog) Speak() string {
return "Woof!"
}
2. Устройство интерфейса "под капотом"
Это любимый вопрос на Middle-позиции. В памяти переменная типа "интерфейс" занимает 16 байт (на 64-битных системах) и представляет собой структуру из двух указателей (по 8 байт каждый).
В исходниках Go (в рантайме) интерфейсы делятся на два типа: eface (пустой интерфейс) и iface (интерфейс с методами).
1. iface (Интерфейс с методами)
Когда мы используем интерфейс, в котором есть хотя бы один метод (например, error или io.Reader), под капотом создается структура iface:
tab(указатель наitab): Это таблица интерфейса. Она хранит информацию о:- Конкретном типе данных, который сейчас лежит в интерфейсе (например,
*Dog). - Самостоятельном типе интерфейса (например,
Speaker). - Списке указателей на конкретные реализации методов (таблица виртуальных методов). Именно благодаря этому Go знает, какую функцию вызвать при обращении
i.Speak().
- Конкретном типе данных, который сейчас лежит в интерфейсе (например,
data(указатель на данные): Это указатель на саму копию данных (или на оригинал, если в интерфейс передали указатель), например, на экземпляр структурыDog.
2. eface (Пустой интерфейс interface{} или any)
Пустой интерфейс не имеет методов, поэтому ему не нужна сложная таблица itab для вызова функций. Под капотом это структура eface:
_type: Указатель на информацию о типе значения, которое в данный момент хранится в интерфейсе.data: Указатель на сами данные.
3. Главная ловушка на собеседовании: nil-интерфейс
Из-за того, что интерфейс состоит из двух указателей (tab и data), возникает самое популярное неочевидное поведение в Go.
Правило: Интерфейс равен nil только тогда, когда оба его внутренних указателя (tab и data) равны nil.
Посмотри на этот код (его часто дают в виде задачки):
func main() {
var p *bytes.Buffer = nil // Обычный типизированный указатель (равен nil)
var i io.Writer = p // Кладем типизированный nil в интерфейс
if i == nil {
fmt.Println("i is nil")
} else {
fmt.Println("i is NOT nil")
}
}
Ответ: Выведется i is NOT nil!
Почему так происходит?
Когда мы присваиваем p в i, интерфейс заполняет свои внутренние поля:
tab(тип) =*bytes.Buffer(тип известен!)data(значение) =nilТак как полеtabне равноnil, весь интерфейсiбольше не равенnil.
Как чинить? Никогда не возвращай конкретный типизированный nil из функции, которая возвращает интерфейс. Возвращай просто nil.
// ПЛОХО: Вернет не-nil интерфейс, если произошла ошибка
func DoSomething() error {
var customErr *MyError = nil
return customErr
}
// ХОРОШО:
func DoSomething() error {
return nil
}
4. Зачем нужен пустой интерфейс (any)?
До версии Go 1.18 везде использовался interface{}. Начиная с 1.18 добавили алиас any. Это одно и то же.
Так как пустой интерфейс не требует реализации никаких методов, абсолютно любой тип в Go реализует пустой интерфейс.
Он используется, когда функция должна принимать данные неизвестного заранее типа (как функция fmt.Println, которая принимает ...any).
Но работать с пустым интерфейсом напрямую нельзя (мы не знаем, что внутри). Чтобы достать значение, используют:
- Type Assertion (Утверждение типа):
val, ok := i.(string) - Type Switch:
switch v := i.(type) {
case int:
fmt.Println("Это число", v)
case string:
fmt.Println("Это строка", v)
}
Резюме
- Интерфейс — это контракт на поведение, реализуется неявно (Duck Typing).
- В памяти это 16 байт: указатель на тип/методы (
itab) и указатель на данные (data). - Интерфейс
== nilтолько если и тип, и данныеnil. Типизированныйnilвнутри интерфейса делает его не равнымnil. - Динамическая диспетчеризация (вызов нужного метода) происходит в рантайме через
itab. Это чуть медленнее, чем прямой вызов метода структуры, поэтому не стоит делать интерфейсы там, где они не нужны для полиморфизма.