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

Введение

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

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

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

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

Одна из основных реализаций компоновки – использование интерфейсов. Интерфейс определяет поведение типа. Один из самых commonly used interfaces in the Go standard library is the fmt.Stringer interface:

type Stringer interface {
    String() string
}

The first line of code defines a type called Stringer. It then states that it is an interface. Just like defining a struct, Go uses curly braces ({}) to surround the definition of the interface. In comparison to defining structs, we only define the interface’s behavior; that is, “what can this type do”.

In the case of the Stringer interface, the only behavior is the String() method. The method takes no arguments and returns a string.

Next, let’s look at some code that has the fmt.Stringer behavior:

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())
}

The first thing we do is create a new type called Article. This type has a Title and an Author field and both are of the string data type:

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

Next, we define a method called String on the Article type. The String method will return a string that represents the Article type:

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

Преобразованный текст в русский язык будет выглядят так:

Тогда, во <код>main функции, мы создаем экземпляр типа Article и присваиваем его значение variables под названием 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())

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

Output
The "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. Метод – это специальная функция, обладающая областью действия для конкретного типа в Go. В отличие от функции, метод может вызываться только из экземпляра типа, на котором он был определен.

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

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

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:

Output
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 (область). Этот метод вернет геометрическую область их соответствующих фигур:

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

Примечание: Идиоматически считается, что для именования интерфейсов следует добавлять суффикс 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())
}

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

Output
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 метод любого аргумента, который был передан ему.

КонCLЮЗИЯ

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

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

Source:
https://www.digitalocean.com/community/tutorials/how-to-use-interfaces-in-go