Интерфейсы - контракты в Go
Интерфейсы - контракты в Go
Интерфейсы в Go - это то, что делает язык по-настоящему гибким. Они определяют контракт (набор методов), но не реализацию. И самое крутое - реализация неявная!
Основы интерфейсов
Объявление интерфейса
type Writer interface {
Write([]byte) (int, error)
}
type Closer interface {
Close() error
}
// Композиция интерфейсов
type WriteCloser interface {
Writer
Closer
}
Неявная реализация
type File struct {
name string
}
// File автоматически реализует Writer
func (f *File) Write(data []byte) (int, error) {
fmt.Printf("Writing to %s: %s\n", f.name, string(data))
return len(data), nil
}
// Использование
var w Writer = &File{name: "test.txt"}
w.Write([]byte("Hello, Go!"))
Что лежит внутри интерфейсного значения
Когда вы пишете var w io.Writer = file, переменная w - это пара указателей: на таблицу типа с методами (itab) и на сами данные. Поэтому интерфейс всегда занимает 16 байт на 64-битной платформе, а вызов w.Write(...) идёт через косвенный диспатч.
itab кэшируется в runtime: для каждой пары (тип, интерфейс) создаётся одна таблица и переиспользуется.
Утиная типизация (duck typing)
"If it walks like a duck and quacks like a duck, it's a duck."
Тип «является» интерфейсом, если у него есть нужные методы - без явных объявлений вроде implements и без наследования. Формально это структурная типизация (проверку делает компилятор), но идея та же, что у динамических языков: важно поведение, а не иерархия.
Главное практическое следствие: интерфейс описывает потребитель, а не поставщик. Можно объявить свой интерфейс уже после того, как чужой тип написан - и он автоматически подойдёт, если сигнатуры совпали.
func (u *User) String() string { return u.Name }
var s fmt.Stringer
s = &User{Name: "Иван"} // OK
s = User{Name: "Иван"} // compile error: метод есть только у *User
Пустой интерфейс
// interface{} или any (Go 1.18+)
var i interface{} = 42
i = "hello"
i = []int{1, 2, 3}
// Функция, принимающая что угодно
func Print(v any) {
fmt.Println(v)
}
Type assertion и type switch
Type assertion
var i interface{} = "hello"
// Безопасное преобразование
s, ok := i.(string)
if ok {
fmt.Println("Строка:", s)
}
// Паника при неудаче
s2 := i.(string) // OK
n := i.(int) // panic!
Type switch
Подробнее про обычный switch и type switch - в уроке if, switch и логика решений.
func describe(i interface{}) {
switch v := i.(type) {
case string:
fmt.Printf("Строка длиной %d: %s\n", len(v), v)
case int:
fmt.Printf("Число: %d\n", v)
case bool:
fmt.Printf("Булево: %t\n", v)
default:
fmt.Printf("Неизвестный тип: %T\n", v)
}
}
Стандартные интерфейсы
io.Reader и io.Writer
type Reader interface {
Read([]byte) (int, error)
}
type Writer interface {
Write([]byte) (int, error)
}
// Пример реализации
type UpperWriter struct {
w io.Writer
}
func (u *UpperWriter) Write(data []byte) (int, error) {
return u.w.Write(bytes.ToUpper(data))
}
// Композиция
upper := &UpperWriter{w: os.Stdout}
fmt.Fprintln(upper, "hello world") // HELLO WORLD
fmt.Stringer
type Stringer interface {
String() string
}
type Point struct {
X, Y int
}
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
// Автоматически используется в fmt
p := Point{3, 4}
fmt.Println(p) // (3, 4)
error
type error interface {
Error() string
}
// Собственный тип ошибки
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}
Проектирование интерфейсов
Маленькие интерфейсы
// Хорошо: маленькие, сфокусированные интерфейсы
type Reader interface {
Read([]byte) (int, error)
}
type Writer interface {
Write([]byte) (int, error)
}
// Плохо: большой интерфейс
type FileSystem interface {
Open(string) (File, error)
Create(string) (File, error)
Remove(string) error
Rename(string, string) error
Stat(string) (FileInfo, error)
// ... еще 10 методов
}
Композиция интерфейсов
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
type ReadWriteSeeker interface {
Reader
Writer
Seeker
}
Где интерфейсы работают на практике
Интерфейсы - фундамент для большинства архитектурных приёмов и паттернов проектирования. Здесь важно понять, как они устроены; куда их применять - разбираем в отдельных треках:
- Strategy - взаимозаменяемые алгоритмы за единым контрактом (оплата картой/через PayPal/криптой). Подробно в уроке GoF · Strategy.
- Decorator - добавление поведения без изменения исходного типа (логирование, кеш, ретраи поверх существующего сервиса). См. GoF · Decorator.
- Adapter - приведение чужого API к нашему интерфейсу. См. GoF · Adapter.
- Dependency Injection - сервис принимает зависимости через интерфейсы, а не создаёт конкретные типы внутри себя. Это даёт тестируемость и слабую связанность. См. Hex Architecture · Ports & Adapters и Hex Architecture · Testing.
- Mock-объекты в тестах - подменяем реальную зависимость заглушкой, реализующей тот же интерфейс. Пример смотри в секции «Testing с интерфейсами» ниже.
Каноничный пример из стандартной библиотеки - HTTP middleware на базе http.Handler - разобран отдельной секцией ниже: он полезен сам по себе, потому что встретится в треке про HTTP.
Nil интерфейсы
type Writer interface {
Write([]byte) (int, error)
}
var w Writer // w == nil, тип и значение nil
type myWriter struct{}
func (m *myWriter) Write(p []byte) (int, error) {
return len(p), nil
}
var mw *myWriter // mw == nil
w = mw // w != nil, но значение внутри nil!
if w != nil {
w.Write([]byte("test")) // panic если метод не проверяет receiver
}
Практический пример: HTTP middleware
type Middleware func(http.Handler) http.Handler
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func Authentication(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// Композиция
handler := http.HandlerFunc(myHandler)
handler = Logging(Authentication(handler))
Testing с интерфейсами
type Database interface {
Get(id string) (string, error)
Set(id, value string) error
}
type MockDatabase struct {
data map[string]string
}
func (m *MockDatabase) Get(id string) (string, error) {
if val, ok := m.data[id]; ok {
return val, nil
}
return "", errors.New("not found")
}
func (m *MockDatabase) Set(id, value string) error {
m.data[id] = value
return nil
}
// В тестах используем mock
func TestService(t *testing.T) {
db := &MockDatabase{data: make(map[string]string)}
service := NewService(db)
// тестируем...
}
Best practices
<ComparisonTable data={{ headers: ["Практика", "Плохо", "Хорошо"], rows: [ ["Размер интерфейса", "type Storage interface { /* 15 методов */ }", "type Reader interface { Read([]byte) (int, error) }"], ["Именование", "type WriterInterface interface {}", "type Writer interface {}"], ["Проверка реализации", "// никакой проверки", "var _ Writer = (*MyWriter)(nil)"], ["Возврат интерфейсов", "func New() *ConcreteType", "func New() Interface // если нужна гибкость"] ] }} />
Итоги
- Интерфейсы определяют поведение, не реализацию
- Реализация неявная (duck typing)
- Маленькие интерфейсы - хорошие интерфейсы
- interface{} для универсальности
- Мощный инструмент для тестирования
В следующем уроке разберем обработку ошибок - философию Go!
Типичная ошибка
Делать «универсальный интерфейс на всё». Это как швейцарский нож... но без ножа.