Как использовать интерфейсы в Go

Вступление

Написание гибкого, многократно используемого и модульного кода жизненно важно для разработки универсальных программ. Работа таким образом гарантирует, что код легче поддерживать, избегая необходимости вносить одно и то же изменение в нескольких местах. Как вы этого достигнете, зависит от языка. Например,inheritance - это общий подход, который используется в таких языках, как Java, C ++, C # и других.

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

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

Определение поведения

Одной из основных реализаций композиции является использование интерфейсов. Интерфейс определяет поведение типа. Одним из наиболее часто используемых интерфейсов в стандартной библиотеке Go является интерфейсfmt.Stringer:

type Stringer interface {
    String() string
}

Первая строка кода определяетtype с именемStringer. Затем он заявляет, что этоinterface. Как и при определении структуры, Go использует фигурные скобки ({}), чтобы окружить определение интерфейса. По сравнению с определением структур мы определяем только интерфейсbehavior; то есть «что может сделать этот тип».

В случае интерфейсаStringer единственным поведением является методString(). Метод не принимает аргументов и возвращает строку.

Затем давайте посмотрим на код, который имеет поведениеfmt.Stringer:

main.go

package main

import "fmt"

type Article struct {
    Title string
    Author string
}

func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

func main() {
    a := Article{
        Title: "Understanding Interfaces in Go",
        Author: "Sammy Shark",
    }
    fmt.Println(a.String())
}

Первое, что мы делаем, это создаем новый тип с именемArticle. Этот тип имеет поляTitle иAuthor, и оба относятся к строкеdata type:

main.go

...
type Article struct {
    Title string
    Author string
}
...

Затем мы определяемmethod с именемString для типаArticle. МетодString вернет строку, представляющую типArticle:

main.go

...
func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}
...

Затем в нашемmainfunction мы создаем экземпляр типаArticle и назначаем егоvariable с именемa. Мы предоставляем значения"Understanding Interfaces in Go" для поляTitle и"Sammy Shark" для поляAuthor:

main.go

...
a := Article{
    Title: "Understanding Interfaces in Go",
    Author: "Sammy Shark",
}
...

Затем мы распечатываем результат методаString, вызываяfmt.Println и передавая результат вызова методаa.String():

main.go

...
fmt.Println(a.String())

После запуска программы вы увидите следующий вывод:

OutputThe "Understanding Interfaces in Go" article was written by Sammy Shark.

До сих пор мы не использовали интерфейс, но мы создали тип с поведением. Такое поведение соответствовало интерфейсуfmt.Stringer. Далее, давайте посмотрим, как мы можем использовать это поведение, чтобы сделать наш код более пригодным для повторного использования.

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

Теперь, когда у нас есть определенный тип с желаемым поведением, мы можем посмотреть, как использовать это поведение.

Однако прежде чем мы это сделаем, давайте посмотрим, что нам нужно сделать, если мы хотим вызвать методString из типаArticle в функции:

main.go

package main

import "fmt"

type Article struct {
    Title string
    Author string
}

func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

func main() {
    a := Article{
        Title: "Understanding Interfaces in Go",
        Author: "Sammy Shark",
    }
    Print(a)
}

func Print(a Article) {
    fmt.Println(a.String())
}

В этом коде мы добавляем новую функцию с именемPrint, которая принимаетArticle в качестве аргумента. Обратите внимание, что единственное, что делает функцияPrint, - это вызывает методString. Из-за этого мы могли бы вместо этого определить интерфейс для передачи в функцию:

main.go

package main

import "fmt"

type Article struct {
    Title string
    Author string
}

func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

type Stringer interface {
    String() string
}

func main() {
    a := Article{
        Title: "Understanding Interfaces in Go",
        Author: "Sammy Shark",
    }
    Print(a)
}

func Print(s Stringer) {
    fmt.Println(s.String())
}

Здесь мы создали интерфейс под названиемStringer:

main.go

...
type Stringer interface {
    String() string
}
...

ИнтерфейсStringer имеет только один метод, называемыйString(), который возвращаетstring. method - это специальная функция, привязанная к определенному типу в Go. В отличие от функции, метод может быть вызван только из экземпляра типа, для которого он был определен.

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

Теперь мы можем использовать методPrint со всем, что удовлетворяет интерфейсуStringer. Давайте создадим другой тип, чтобы продемонстрировать это:

main.go

package main

import "fmt"

type Article struct {
    Title  string
    Author string
}

func (a Article) String() string {
    return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}

type Book struct {
    Title  string
    Author string
    Pages  int
}

func (b Book) String() string {
    return fmt.Sprintf("The %q book was written by %s.", b.Title, b.Author)
}

type Stringer interface {
    String() string
}

func main() {
    a := Article{
        Title:  "Understanding Interfaces in Go",
        Author: "Sammy Shark",
    }
    Print(a)

    b := Book{
        Title:  "All About Go",
        Author: "Jenny Dolphin",
        Pages:  25,
    }
    Print(b)
}

func Print(s Stringer) {
    fmt.Println(s.String())
}

Теперь мы добавляем второй тип с именемBook. В нем также определен методString. Это означает, что он также удовлетворяет интерфейсуStringer. Из-за этого мы также можем отправить его в нашу функциюPrint:

OutputThe "Understanding Interfaces in Go" article was written by Sammy Shark.
The "All About Go" book was written by Jenny Dolphin. It has 25 pages.

До сих пор мы демонстрировали, как использовать только один интерфейс. Однако интерфейс может иметь более одного определенного поведения. Далее мы увидим, как мы можем сделать наши интерфейсы более универсальными, объявив больше методов.

Несколько Поведений в Интерфейсе

Одним из основных принципов написания кода Go является написание небольших, кратких типов и их составление до более крупных и более сложных типов. То же самое верно при составлении интерфейсов. Чтобы увидеть, как мы создаем интерфейс, мы сначала начнем с определения только одного интерфейса. Мы определим две фигуры,Circle иSquare, и обе они будут определять метод с именемArea. Этот метод вернет геометрическую область их соответствующих форм:

main.go

package main

import (
    "fmt"
    "math"
)

type Circle struct {
    Radius float64
}

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

type Square struct {
    Width  float64
    Height float64
}

func (s Square) Area() float64 {
    return s.Width * s.Height
}

type Sizer interface {
    Area() float64
}

func main() {
    c := Circle{Radius: 10}
    s := Square{Height: 10, Width: 5}

    l := Less(c, s)
    fmt.Printf("%+v is the smallest\n", l)
}

func Less(s1, s2 Sizer) Sizer {
    if s1.Area() < s2.Area() {
        return s1
    }
    return s2
}

Поскольку каждый тип объявляет методArea, мы можем создать интерфейс, определяющий это поведение. Создаем следующий интерфейсSizer:

main.go

...
type Sizer interface {
    Area() float64
}
...

Затем мы определяем функцию с именемLess, которая принимает дваSizer и возвращает наименьшее из них:

main.go

...
func Less(s1, s2 Sizer) Sizer {
    if s1.Area() < s2.Area() {
        return s1
    }
    return s2
}
...

Обратите внимание, что мы не только принимаем оба аргумента как типSizer, но также возвращаем результат какSizer. Это означает, что мы больше не возвращаемSquare илиCircle, а возвращаем интерфейсSizer.

Наконец, мы распечатаем то, что имело наименьшую площадь:

Output{Width:5 Height:10} is the smallest

Далее давайте добавим другое поведение к каждому типу. На этот раз мы добавим методString(), который возвращает строку. Это удовлетворит интерфейсfmt.Stringer:

main.go

package main

import (
    "fmt"
    "math"
)

type Circle struct {
    Radius float64
}

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

func (c Circle) String() string {
    return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius)
}

type Square struct {
    Width  float64
    Height float64
}

func (s Square) Area() float64 {
    return s.Width * s.Height
}

func (s Square) String() string {
    return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height)
}

type Sizer interface {
    Area() float64
}

type Shaper interface {
    Sizer
    fmt.Stringer
}

func main() {
    c := Circle{Radius: 10}
    PrintArea(c)

    s := Square{Height: 10, Width: 5}
    PrintArea(s)

    l := Less(c, s)
    fmt.Printf("%v is the smallest\n", l)

}

func Less(s1, s2 Sizer) Sizer {
    if s1.Area() < s2.Area() {
        return s1
    }
    return s2
}

func PrintArea(s Shaper) {
    fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
}

Поскольку иCircle, и типSquare реализуют методыArea иString, теперь мы можем создать другой интерфейс для описания этого более широкого набора поведения. Для этого мы создадим интерфейс под названиемShaper. Мы составим это из интерфейсаSizer и интерфейсаfmt.Stringer:

main.go

...
type Shaper interface {
    Sizer
    fmt.Stringer
}
...

[.note] #Note: Считается идиоматичным пытаться назвать свой интерфейс, заканчивая наer, напримерfmt.Stringer,io.Writer и т. д. Вот почему мы назвали наш интерфейсShaper, а неShape.
#

Теперь мы можем создать функцию с именемPrintArea, которая принимаетShaper в качестве аргумента. Это означает, что мы можем вызвать оба метода для переданного значения как для методаArea, так и для методаString:

main.go

...
func PrintArea(s Shaper) {
    fmt.Printf("area of %s is %.2f\n", s.String(), s.Area())
}

Если мы запустим программу, мы получим следующий вывод:

Outputarea of Circle {Radius: 10.00} is 314.16
area of Square {Width: 5.00, Height: 10.00} is 50.00
Square {Width: 5.00, Height: 10.00} is the smallest

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

Например, если мы передалиShaper функцииLess, мы можем предположить, что она вызовет как методыArea, так иString. Однако, поскольку мы намереваемся вызвать только методArea, он делает функциюLess понятной, поскольку мы знаем, что мы можем вызывать методArea только для любого переданного ему аргумента.

Заключение

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

Если вы хотите узнать больше о языке программирования Go, ознакомьтесь сHow To Code in Go series целиком.