Интерфейсы и полиморфизм

5. Интерфейсы и полиморфизм #

5.1 Что такое интерфейсы? #

В Go интерфейсы — это набор сигнатур методов, которые должен реализовать тип, чтобы соответствовать этому интерфейсу. Основное преимущество интерфейсов в Go — неявная реализация. Тип автоматически удовлетворяет интерфейсу, если реализует все его методы.

Основные свойства: #

  • Интерфейсы описывают поведение, а не структуру.
  • Любой тип, который реализует методы интерфейса, может быть использован там, где требуется этот интерфейс.
  • Неявная реализация избавляет от необходимости явно указывать, что тип реализует интерфейс.

Пример интерфейса:

type Shape interface {
    Area() float64
}

5.2 Реализация интерфейсов #

Для реализации интерфейса тип должен предоставить все его методы.

Пример:

type Circle struct {
    Radius float64
}

// Метод для вычисления площади круга
func (c Circle) Area() float64 {
    return 3.14 * c.Radius * c.Radius
}

type Rectangle struct {
    Width, Height float64
}

// Метод для вычисления площади прямоугольника
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

Использование интерфейса:

func printArea(s Shape) {
    fmt.Println("Площадь:", s.Area())
}

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

    printArea(c) // Площадь: 78.5
    printArea(r) // Площадь: 24
}

5.3 Полиморфизм через интерфейсы #

Полиморфизм позволяет работать с объектами разных типов через общий интерфейс.

Пример:

type Printer interface {
    Print()
}

type Text struct {
    Content string
}

func (t Text) Print() {
    fmt.Println("Text:", t.Content)
}

type Image struct {
    FileName string
}

func (i Image) Print() {
    fmt.Println("Image:", i.FileName)
}

func printAll(items []Printer) {
    for _, item := range items {
        item.Print()
    }
}

func main() {
    text := Text{Content: "Hello, Go!"}
    image := Image{FileName: "picture.jpg"}

    items := []Printer{text, image}
    printAll(items)
}

5.4 Интерфейсы в стандартной библиотеке #

Go активно использует интерфейсы в стандартной библиотеке.

Примеры интерфейсов: #

  1. io.Reader и io.Writer: Для чтения и записи данных.
    func copyContent(src io.Reader, dst io.Writer) {
        io.Copy(dst, src)
    }
    
  2. error: Для представления ошибок.
    type error interface {
        Error() string
    }
    

Реализация интерфейса error: #

type MyError struct {
    Message string
}

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

func main() {
    err := MyError{Message: "Something went wrong"}
    fmt.Println(err.Error()) // Something went wrong
}

5.5 Пустой интерфейс (interface{}) #

Пустой интерфейс может содержать значение любого типа, так как все типы реализуют интерфейс без методов.

Использование пустого интерфейса: #

  1. Хранение любых значений:

    var data interface{}
    data = 42
    fmt.Println(data) // 42
    data = "Hello"
    fmt.Println(data) // Hello
    
  2. Type assertion: Для извлечения значения из пустого интерфейса.

    value, ok := data.(int)
    if ok {
        fmt.Println("Целое число:", value)
    } else {
        fmt.Println("Не целое число")
    }
    
  3. Type switch: Для определения типа значения.

    switch v := data.(type) {
    case int:
        fmt.Println("int:", v)
    case string:
        fmt.Println("string:", v)
    default:
        fmt.Println("Unknown type")
    }
    

5.6 Комбинирование интерфейсов #

Интерфейсы могут быть составными. Это позволяет определять поведение, комбинируя методы нескольких интерфейсов.

Пример:

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

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

// ReadWriter объединяет Reader и Writer
type ReadWriter interface {
    Reader
    Writer
}

5.7 Советы по работе с интерфейсами #

  1. Используйте интерфейсы для абстракции: Разрабатывайте код, опираясь на интерфейсы, а не на конкретные типы.

  2. Избегайте избыточности: Определяйте интерфейсы только с необходимыми методами.

  3. Интерфейсы и тестирование: Интерфейсы упрощают создание тестов, позволяя подменять реальные реализации mock-объектами.

Пример с тестированием:

type Database interface {
    Query(query string) string
}

// Mock для тестирования
type MockDB struct{}

func (db MockDB) Query(query string) string {
    return "Mocked result"
}

func fetchData(db Database) string {
    return db.Query("SELECT * FROM table")
}

func main() {
    mock := MockDB{}
    fmt.Println(fetchData(mock)) // Mocked result
}

5.8 Интерфейсы и дженерики #

С появлением дженериков (с версии Go 1.18) использование интерфейсов стало ещё мощнее.

Пример с ограничением типов через интерфейсы:

type Number interface {
    int | float64
}

func Sum[T Number](a, b T) T {
    return a + b
}

func main() {
    fmt.Println(Sum(3, 5))       // 8
    fmt.Println(Sum(2.5, 3.1))   // 5.6
}

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