Для разработки гибких и универсальных программ очень важно создавать гибкий, модульный и многоразовый код. Такая модель работы упрощает обслуживание кода, устраняя необходимость вносить одинаковые изменения в разных местах. Конкретный способ достижения цели зависит от языка. Например, концепция наследования представляет собой распространенный подход, используемый в таких языках как 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
, оба из которых относятся к строковому типу данных:
...
type Article struct {
Title string
Author string
}
...
Далее мы определим метод
с именем String
для типа Article
. Метод String
будет возвращать строку, представляющую тип Article
:
...
func (a Article) String() string {
return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
}
...
Затем в функции main
мы создадим экземпляр типа Article
и назначим его для переменной с именем a
. Мы зададим значение "Understanding Interfaces in Go"
для поля Title
и значение "Sammy Shark"
для поля Author
:
...
a := Article{
Title: "Understanding Interfaces in Go",
Author: "Sammy Shark",
}
...
Затем мы выведем результат метода String
посредством вызова fmt.Println
и передачи результата вызова метода a.String()
:
...
fmt.Println(a.String())
После запуска программы вы увидите следующее:
OutputThe "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
для использования String
er, а не конкретного типа 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
:
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
. Этот метод будет возвращать геометрическую площадь соответствующих фигур:
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
:
...
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
.
Наконец, мы распечатываем объект с наименьшей площадью:
Output{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())
}
Если мы запустим программу, результат будет выглядеть следующим образом:
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, ознакомьтесь с нашей серией статей о программировании на языке Go.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!