MidТеория6 min

Основы интерфейсов

Определение интерфейсов, имплицитная реализация, empty interface, композиция и принцип маленьких интерфейсов

Интерфейсы — одна из самых мощных и элегантных концепций Go. Они обеспечивают полиморфизм без наследования, через имплицитную (неявную) реализацию.

Определение интерфейса

Интерфейс определяет набор методов, которые тип должен реализовать:

package main

import (
    "fmt"
    "math"
)

// Interface definition — a contract
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Circle implements Shape implicitly — no "implements" keyword!
type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// Rectangle also implements Shape
type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Function accepting the interface — works with ANY Shape
func printShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

func main() {
    c := Circle{Radius: 5}
    r := Rectangle{Width: 10, Height: 3}

    // Both satisfy the Shape interface
    printShapeInfo(c) // Area: 78.54, Perimeter: 31.42
    printShapeInfo(r) // Area: 30.00, Perimeter: 26.00

    // Slice of interfaces
    shapes := []Shape{c, r, Circle{Radius: 1}}
    totalArea := 0.0
    for _, s := range shapes {
        totalArea += s.Area()
    }
    fmt.Printf("Total area: %.2f\n", totalArea)
}

Имплицитная реализация (Duck Typing)

В Go нет ключевого слова implements. Тип реализует интерфейс автоматически, если имеет все методы интерфейса:

// No "implements" needed — just have the right methods

type Dog struct {
    Name string
}

func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct {
    Name string
}

func (c Cat) Speak() string {
    return "Meow!"
}

// Interface defined AFTER the types — still works!
type Speaker interface {
    Speak() string
}

// Both Dog and Cat implement Speaker without knowing about it
func makeNoise(s Speaker) {
    fmt.Println(s.Speak())
}

Go Proverb: "The bigger the interface, the weaker the abstraction." Чем больше интерфейс, тем слабее абстракция.

Проверка реализации в compile-time

// Compile-time check that Circle implements Shape
var _ Shape = Circle{}       // Value satisfies
var _ Shape = (*Circle)(nil) // Pointer satisfies

// This is a common Go idiom in library code
// If Circle doesn't implement Shape, compilation fails

Правила удовлетворения интерфейса

package main

import "fmt"

type Printer interface {
    Print()
}

type ValueType struct {
    Name string
}

// Value receiver — both T and *T satisfy the interface
func (v ValueType) Print() {
    fmt.Println(v.Name)
}

type PointerType struct {
    Name string
}

// Pointer receiver — only *T satisfies the interface
func (p *PointerType) Print() {
    fmt.Println(p.Name)
}

func main() {
    var p Printer

    // Value receiver: both value and pointer work
    p = ValueType{Name: "value"}   // OK
    p.Print()
    p = &ValueType{Name: "ptr"}    // OK
    p.Print()

    // Pointer receiver: ONLY pointer works
    // p = PointerType{Name: "value"} // COMPILE ERROR!
    p = &PointerType{Name: "ptr"}  // OK
    p.Print()
}
Метод определён с T удовлетворяет *T удовлетворяет
Value receiver (t T) Да Да
Pointer receiver (t *T) Нет Да

Почему? Если метод имеет pointer receiver, значению типа T не всегда можно взять адрес (например, элемент карты). Поэтому Go требует *T.

Empty Interface: interface{} / any

Пустой интерфейс не содержит методов — его удовлетворяет любой тип:

package main

import "fmt"

func main() {
    // interface{} — old syntax
    var anything interface{}
    anything = 42
    anything = "hello"
    anything = []int{1, 2, 3}
    anything = struct{ Name string }{"Go"}

    // any — alias for interface{} (Go 1.18+)
    var value any
    value = 42
    value = "hello"
    value = true

    // Common use: variadic functions accepting any type
    printAll(1, "hello", true, 3.14)

    // To use the value, you need type assertion
    if s, ok := value.(string); ok {
        fmt.Println("String:", s)
    }

    fmt.Println(anything, value)
}

func printAll(args ...any) {
    for _, arg := range args {
        fmt.Printf("%v (%T)\n", arg, arg)
    }
}

Когда (НЕ) использовать interface{} / any

Использование Пример Рекомендация
JSON parsing map[string]any OK
Generics заменитель func Process(v any) Используйте дженерики
Printf-like functions func Log(msg string, args ...any) OK
Коллекция разных типов []any Осторожно, рассмотрите интерфейс
API parameter func Handle(data any) Определите конкретный интерфейс

Совет: С появлением дженериков (Go 1.18+) необходимость в interface{} / any значительно снизилась. Предпочитайте типизированные решения.

Композиция интерфейсов (Embedding)

Go поощряет маленькие интерфейсы, которые можно комбинировать:

package main

import (
    "fmt"
    "io"
)

// Small, focused interfaces
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

// Composed interfaces through embedding
type ReadWriter interface {
    Reader
    Writer
}

type ReadCloser interface {
    Reader
    Closer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

// Real-world example: custom interface composition
type Logger interface {
    Log(level string, msg string)
}

type Metrics interface {
    RecordLatency(operation string, duration float64)
    IncrementCounter(name string)
}

type Observable interface {
    Logger
    Metrics
}

// A service that requires observability
type UserService struct {
    obs Observable
}

func NewUserService(obs Observable) *UserService {
    return &UserService{obs: obs}
}

func (s *UserService) GetUser(id int) {
    s.obs.Log("INFO", fmt.Sprintf("Getting user %d", id))
    s.obs.IncrementCounter("user.get")
    // ... fetch user
}

Стандартная библиотека — отличный пример композиции:

io.Reader     ──┐
                ├── io.ReadWriter     ──┐
io.Writer     ──┘                       │
                                        ├── io.ReadWriteCloser
io.Closer     ──────────────────────────┘

Принцип маленьких интерфейсов

"Accept interfaces, return structs." — Go Proverb

// GOOD: small, focused interfaces
type Saver interface {
    Save(data []byte) error
}

type Loader interface {
    Load(key string) ([]byte, error)
}

// Functions accept only what they need
func backup(s Saver, data []byte) error {
    return s.Save(data)
}

func restore(l Loader, key string) ([]byte, error) {
    return l.Load(key)
}

// BAD: large kitchen-sink interface
type Storage interface {
    Save(data []byte) error
    Load(key string) ([]byte, error)
    Delete(key string) error
    List() ([]string, error)
    Exists(key string) bool
    GetMetadata(key string) (map[string]string, error)
    SetMetadata(key string, meta map[string]string) error
}

Интерфейсы из 1-2 методов в stdlib

Интерфейс Методы Пакет
io.Reader Read([]byte) (int, error) io
io.Writer Write([]byte) (int, error) io
io.Closer Close() error io
fmt.Stringer String() string fmt
error Error() string builtin
sort.Interface Len(), Less(), Swap() sort
http.Handler ServeHTTP(ResponseWriter, *Request) net/http
json.Marshaler MarshalJSON() ([]byte, error) encoding/json
context.Context 4 метода context

nil интерфейсы vs интерфейсы, содержащие nil

Это одна из самых коварных ловушек в Go:

package main

import "fmt"

type MyError struct {
    Message string
}

func (e *MyError) Error() string {
    return e.Message
}

func getError(fail bool) error {
    var err *MyError // Typed nil pointer

    if fail {
        err = &MyError{Message: "something failed"}
    }

    return err // TRAP: returns non-nil interface holding nil pointer!
}

func main() {
    err := getError(false)

    // This is NEVER nil, even when err is supposed to be nil!
    if err != nil {
        fmt.Println("Error:", err) // PANIC: calling Error() on nil *MyError
    }
}

Объяснение

Интерфейс в Go состоит из двух указателей: (type, value).

nil interface:              (nil, nil)     → == nil ✓
interface holding nil:      (*MyError, nil) → != nil ✗ (type is not nil!)
interface holding value:    (*MyError, 0xc0000a2000) → != nil ✗

Правильный подход

func getError(fail bool) error {
    if fail {
        return &MyError{Message: "something failed"}
    }
    return nil // Return nil interface directly, NOT typed nil
}

// Or use explicit nil check before returning
func getErrorSafe(fail bool) error {
    var err *MyError
    if fail {
        err = &MyError{Message: "something failed"}
    }
    if err == nil {
        return nil // Explicitly return nil interface
    }
    return err
}

Правило: Никогда не возвращайте типизированный nil через интерфейс. Всегда возвращайте return nil напрямую для «нет ошибки».

Accept Interfaces, Return Structs

package main

import (
    "context"
    "fmt"
)

// GOOD: Accept interface — flexible, testable
type UserRepository interface {
    FindByID(ctx context.Context, id int) (*User, error)
}

type UserService struct {
    repo UserRepository // Accept interface
}

func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo} // Return struct
}

// Concrete implementation
type PostgresUserRepo struct {
    connString string
}

func NewPostgresUserRepo(connString string) *PostgresUserRepo {
    return &PostgresUserRepo{connString: connString} // Return struct
}

func (r *PostgresUserRepo) FindByID(ctx context.Context, id int) (*User, error) {
    // ... database query
    return &User{ID: id, Name: "Alice"}, nil
}

// For testing — mock implementation
type MockUserRepo struct {
    Users map[int]*User
}

func (m *MockUserRepo) FindByID(ctx context.Context, id int) (*User, error) {
    user, ok := m.Users[id]
    if !ok {
        return nil, fmt.Errorf("user %d not found", id)
    }
    return user, nil
}

type User struct {
    ID   int
    Name string
}

Этот паттерн обеспечивает:

  • Тестируемость — легко подставить mock
  • Гибкость — легко заменить реализацию
  • Явные зависимости — видно, что сервису нужно
  • Маленькие интерфейсы — каждый потребитель определяет свой интерфейс

Проверь себя

Почему `getError` может вернуть «не nil» ошибку, даже если внутренний указатель nil? ```go func getError() error { var err *MyError // nil return err } ```

Тип имеет метод с pointer receiver. Удовлетворяет ли значение этого типа (не указатель) интерфейсу?

Какой принцип Go рекомендует для параметров и возвращаемых значений функций?

Что такое `any` в Go 1.18+?

Как тип в Go реализует интерфейс?

Code Challenges

Интерфейс фигур

GO

Определите интерфейс Shape с методом Area() float64. Реализуйте его для Circle и Rectangle.

Test Cases

1. Input: Circle{Radius: 5}.Area()→ Expected: 78.54
2. Input: Rectangle{Width: 3, Height: 4}.Area()→ Expected: 12