One place for hosting & domains

      Использование интерфейсов в 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 smallestn", 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 smallestn", 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 %.2fn", 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 %.2fn", 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.



      Source link