cpp

Интерфейсы

Предположим, у нас есть две структуры с одноименными полями и методами:

type A struct {
   X int
}
type B struct {
  X int
}
func (a A) String() string {
   return "(a A) String()"
}
func (b B) String() string {
   return "(b B) String()"
}

Мы можем создать экземпляры этих структур и сохранить их в переменных с типом структуры:

var a A = A{10}
var b B = B{20}

Если мы попробуем в переменной типа A сохранить экземпляр структуры типа B, то получим сообщение об ошибке. Хотя структуры имеют одноименные поля и методы они являются разными типами данных:

var a A = B{20} // Ошибка!
// cannot use B{...} (type B) as type A in assignment

Теперь посмотрим на очень интересный результат при выводе:

fmt.Println(a)          // (a A) String()
fmt.Println(b)          // (b B) String()

Вы заметили? Указав экземпляры разных структур мы получили результаты выполнения методов String(), при этом явно не вызывая их:

fmt.Println(a.String()) // (a A) String()
fmt.Println(b.String()) // (b B) String()

Почему так произошло? Дело в том, что функция Println() ожидает, что структура реализует интерфейс fmt.Stringer. В этом интерфейсе содержится требование о существовании метода String():

type Stringer interface {
   String() string
}

Если структура содержит метод String(), то она автоматически реализует интерфейс fmt.Stringer. Никаких дополнительных действий, чтобы сообщить о реализации интерфейса, предпринимать не нужно. Это результат так называемой "утиной" типизации, которая гласит: "если это выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка".

Итак, наши структуры A и B на самом деле реализуют интерфейс fmt.Stringer. Метод String() из этого интерфейса автоматически вызывается при выводе и при любом преобразовании объекта в строку. Но это не все преимущества интерфейсов. Мы можем создать переменную с типом интерфейса fmt.Stringer и сохранять в этой переменной экземпляры как структуры A, так и структуры B:

var ia fmt.Stringer = A{10}
var ib fmt.Stringer = B{20}

Теперь мы можем воспользоваться методом String():

fmt.Println(ia.String()) // (a A) String()
fmt.Println(ib.String()) // (b B) String()

Важно понимать, что через переменную с типом интерфейса, доступны только методы, описанные внутри интерфейса. Доступа к полям структуры также нет:

fmt.Println(ia.X) // Ошибка!
// ia.X undefined (type fmt.Stringer has no field or method X)

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

a, ok := ia.(A)
if ok {
   fmt.Println(a.X) // 10
}

Напомню, что преобразование типов мы уже рассматривали в разд. 2.11.

Итак, какое основное преимущество дают нам интерфейсы? Мы можем объявить параметр функции с типом интерфейса и передавать туда экземпляры любых структур, реализующих интерфейс:

func Print(obj fmt.Stringer) {
   fmt.Println(obj.String())
}

Создадим экземпляры структур и вызовем функцию Print():

var a A = A{10}
var b B = B{20}
Print(a)  // (a A) String()
Print(b)  // (b B) String()
var pa *A = &A{10}
var pb *B = &B{20}
Print(pa) // (a A) String()
Print(pb) // (b B) String()

Как видно из примера, в функцию Print() мы можем передать как экземпляр структуры, так и указатель на него. Но это возможно только в том случае, если метод принимает экземпляр структуры по значению. Если метод принимает указатель, то передать в функцию Print() мы сможем только указатель. Давайте переделаем определение метода String() структуры A следующим образом:

func (a *A) String() string {
   return "(a A) String()"
}

Создадим экземпляр структуры и вызовем функцию Print():

var a A = A{10}
// Метод String() автоматически не вызывается!
fmt.Println(a)          // {10}
fmt.Println(a.String()) // (a A) String()
// Print(a) // Ошибка!
// A does not implement fmt.Stringer (String method has pointer receiver)
Print(&a)   // (a A) String()
var pa *A = &A{10}
Print(pa)   // (a A) String()

Создание интерфейса

Объявление интерфейса имеет следующий формат:

type <Название интерфейса> interface {
   <Сигнатура метода 1>
   ...
   <Сигнатура метода N>
}

Пример объявления интерфейса с двумя методами:

type MyInterface interface {
   Show()
   String() string
}

Пример объявления переменной с типом интерфейса:

var i MyInterface
fmt.Println(i) // <nil>

Как видно из результата, если переменной не присвоено значение, то она будет иметь значение nil.

Чтобы реализовать интерфейс, достаточно определить методы, объявленные в интерфейсе, внутри структуры (листинг 11.10). Никаких дополнительных действий, чтобы сообщить о реализации интерфейса, предпринимать не нужно. С одной стороны это очень удобно, но другой стороны сразу не понятно какие интерфейсы реализует структура.

Листинг 11.10. Интерфейсы

package main

import "fmt"

type MyInterface interface {
   Show()
   String() string
}
type A struct {
   X int
}
type B struct {
   X int
}

func main() {
   var a A = A{10}
   var b B = B{20}
   test(a)        // (a A) String() X = 10
                  // (a A) Show() X = 10
   test(b)        // (b B) String() X = 20
                  // (b B) Show() X = 20
   Print(a)       // (a A) String() X = 10
   Print(b)       // (b B) String() X = 20
   // Неявный вызов String()
   fmt.Println(a) // (a A) String() X = 10
   fmt.Println(b) // (b B) String() X = 20
}
func (a A) String() string {
   return fmt.Sprintf("(a A) String() X = %d", a.X)
}
func (b B) String() string {
   return fmt.Sprintf("(b B) String() X = %d", b.X)
}
func (a A) Show() {
   fmt.Printf("(a A) Show() X = %d\n", a.X)
}
func (b B) Show() {
   fmt.Printf("(b B) Show() X = %d\n", b.X)
}
func test(obj MyInterface) {
   fmt.Println(obj.String())
   obj.Show()
}
func Print(obj fmt.Stringer) {
   fmt.Println(obj.String())
}

Учебник Go (Golang)
Учебник Go (Golang) в формате PDF

Помощь сайту

ЮMoney (Yandex-деньги): 410011140483022

ПАО Сбербанк:
Счет: 40817810855006152256
Реквизиты банка:
Наименование: СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК
Корреспондентский счет: 30101810500000000653
БИК: 044030653
КПП: 784243001
ОКПО: 09171401
ОКОНХ: 96130
Скриншот реквизитов

cpp