Tutorial

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

Published on January 24, 2020
Русский
Использование интерфейсов в Go

Введение

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

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

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

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

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

type Stringer interface {
    String() string
}

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

В случае с интерфейсом 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, оба из которых относятся к строковому типу данных:

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

Далее мы определим метод с именем 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)
}
...

Затем в функции main мы создадим экземпляр типа Article и назначим его для переменной с именем 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(), и этот метод возвращает строку. Метод — это специальная функция, относящаяся к определенному типу в 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:

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 для любого переданного аргумента.

Заключение

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

Если вы хотите узнать больше о языке программирования Go, ознакомьтесь с нашей серией статей о программировании на языке Go.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


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!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Become a contributor for community

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

DigitalOcean Documentation

Full documentation for every DigitalOcean product.

Resources for startups and SMBs

The Wave has everything you need to know about building a business, from raising funding to marketing your product.

Get our newsletter

Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

New accounts only. By submitting your email you agree to our Privacy Policy

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.