Работа с интерфейсами в Go

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

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

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

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

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

type Stringer interface {
String() string
}

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

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

Теперь давайте посмотрим на пример кода с поведением fmt.Stringer:

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, и оба они работают со строками.

Читайте также: Основные типы данных в Go

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

Затем мы определяем метод String в типе Article. Этот метод вернет строку, которая представляет тип Article.

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

Затем в функции main мы создаем экземпляр типа Article и присваиваем его переменной а. Полю Title мы предоставляем значение «Understanding Interfaces in Go», а полю Author — значение «Sammy Shark».

Читайте также:

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

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

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

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

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

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

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

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

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

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. Потому вместо этого можно было бы просто определить интерфейс, который передавался бы в функцию:

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:

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

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

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

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

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:

The "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. Этот метод вернет геометрическую площадь соответствующих фигур.

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 и выдает наименьшее значение:

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

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

Давайте выведем на экран наименьшую площадь:

{Width:5 Height:10} is the smallest

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

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:

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

Примечание: По традиции имя интерфейса должно заканчиваться на er, например, fmt.Stringer, io.Writer и т. д. Поэтому в этом примере мы назвали интерфейс Shaper, а не Shape.

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

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

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

area 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.

Заключение

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

Tags: ,