Skip to content

Latest commit

 

History

History
395 lines (287 loc) · 18.5 KB

File metadata and controls

395 lines (287 loc) · 18.5 KB

2.6 Интерфейсы

Интерфейсы

Интерфейсы - одна из наиболее тончайших черт дизайна Go. После прочтения этого раздела Вы, скорее всего, будете впечатлены тем, как они реализованы в Go.

Что такое интерфейс

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

Например, в предыдущем примере и у Student(студент), и у Employee(работник) есть метод SayHi(), но они делают не одно и то же.

Давайте кое-что сделаем. Мы добавим им еще один метод Sing()(Петь), а также метод BorrowMoney()(Взять взаймы денег) типу Student и метод SpendSalary()(Потратить з/плату) типу Employee.

Теперь у Student есть три метода: SayHi(), Sing() и BorrowMoney(), а у Employee есть SayHi(), Sing() и SpendSalary().

Такая комбинация методов называется интерфейсом и реализуется как Student, так и Employee. Итак, Student и Employee реализуют интерфейс: SayHi() и Sing(). В то же время Employee не реализует интерфейс: SayHi(), Sing(), BorrowMoney(), а Student не реализует интерфейс: SayHi(), Sing(), SpendSalary(). Это потому, что у Employee нет метода BorrowMoney(), а у Student нет метода SpendSalary().

Тип "Interface"

Интерфейс определяет набор методов, поэтому, если тип реализует эти методы, говорится, что он реализует интерфейс.

type Human struct {
	name  string
	age   int
	phone string
}

type Student struct {
	Human
	school string
	loan   float32
}

type Employee struct {
	Human
	company string
	money   float32
}

func (h *Human) SayHi() {
	fmt.Printf("Привет, я - %s, мой номер телефона - %s\n", h.name, h.phone)
}

func (h *Human) Sing(lyrics string) {
	fmt.Println("Ля ля, ля ля ля, ля ля ля ля ля...", lyrics)
}

func (h *Human) Guzzle(beerStein string) {
	fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}

// Employee перегружает метод SayHi
func (e *Employee) SayHi() {
	fmt.Printf("Привет, я - %s, я работаю в %s. Звоните мне по номеру %s\n", e.name,
		e.company, e.phone) //Да, можно разбить строку на 2 строки.
}

func (s *Student) BorrowMoney(amount float32) {
	s.loan += amount // (снова и снова...)
}

func (e *Employee) SpendSalary(amount float32) {
	e.money -= amount
}

// определяем интерфейс
type Men interface {
	SayHi()
	Sing(lyrics string)
	Guzzle(beerStein string)
}

type YoungChap interface {
	SayHi()
	Sing(song string)
	BorrowMoney(amount float32)
}

type ElderlyGent interface {
	SayHi()
	Sing(song string)
	SpendSalary(amount float32)
}

Интерфейс может быть реализован любым типом данных, и один тип может реализовывать несколько интерфейсов одновременно.

Заметьте, что все типы реализуют пустой интерфейс interface{}, так как у него нет методов, а все типы изначально также не имеют методов.

Значение интерфейса

Итак, какие типы значений может принимать интерфейс? Если мы определили переменную типа interface, то значение любого типа, который реализует этот интерфейс, может быть присвоено этой переменной.

Как в примере выше, если мы определили переменную "m" как интерфейс Men, то все значения типа Student, Human или Employee могут быть присвоены переменной "m". Так что у нас может быть срез элементов типа Men, и значение любого типа, реализующего интерфейс Men, может присвоено элементам этого среза. Но имейте в виду, что срез элементов типа interface не ведет себя так же, как срез из элементов других типов.

package main

import "fmt"

type Human struct {
	name  string
	age   int
	phone string
}

type Student struct {
	Human
	school string
	loan   float32
}

type Employee struct {
	Human
	company string
	money   float32
}

func (h Human) SayHi() {
	fmt.Printf("Привет, я - %s, мой номер телефона - %s\n", h.name, h.phone)
}

func (h Human) Sing(lyrics string) {
	fmt.Println("Ля ля ля ля...", lyrics)
}

func (e Employee) SayHi() {
	fmt.Printf("Привет, я - %s, я работаю в %s. Звоните мне по номеру %s\n", e.name,
		e.company, e.phone) //Да, здесь можно разбить строку на две.
}

// Интерфейс Men реализуется типами Human, Student и Employee
type Men interface {
	SayHi()
	Sing(lyrics string)
}

func main() {
	mike := Student{Human{"Майк", 25, "222-222-XXX"}, "MIT", 0.00}
	paul := Student{Human{"Пол", 26, "111-222-XXX"}, "Harvard", 100}
	sam := Employee{Human{"Сэм", 36, "444-222-XXX"}, "Golang Inc.", 1000}
	tom := Employee{Human{"Сэм", 36, "444-222-XXX"}, "Things Ltd.", 5000}

	// определяем интерфейс i
	var i Men

	//i может быть Student
	i = mike
	fmt.Println("Это Майк, студент:")
	i.SayHi()
	i.Sing("November rain")

	//i может быть Employee
	i = tom
	fmt.Println("Это Том, работник:")
	i.SayHi()
	i.Sing("Born to be wild")

	// срез из элементов типа Men
	fmt.Println("Давайте создадим срез из Men и посмотрим, что получится")
	x := make([]Men, 3)
	// Эти три элемента относятся к разным типам, но все они реализуют интерфейс Men
	x[0], x[1], x[2] = paul, sam, mike

	for _, value := range x {
		value.SayHi()
	}
}

Интерфейс - это набор абстрактных методов, он может быть реализован типами, не являющимися интерфейсами. Поэтому он не может быть реализован самим собой.

Пустой интерфейс

Пустой интерфейс - это интерфейс, который не содержит методов. Это очен полезно, если мы хотим хранить данные любого типа в одном месте, и это похоже на void* в C.

// Определим a как пустой интерфейс
var a interface{}
var i int = 5
s := "Привет, мир!"
// a может принимать значение любого типа
a = i
a = s

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

Интерфейсы как аргументы методов

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

Например, мы много используем fmt.Println, но Вы замечали, что эта команда может принимать в качестве аргумента данные любого типа? Заглянув в исходный код пакета fmt, мы можем найти следующее определение:

type Stringer interface {
		String() string
}

Это значит, что любой тип, реализующий интерфейс Stringer, может быть передан в качестве аргумента в fmt.Println. Давайте докажем это:

package main

import (
	"fmt"
	"strconv"
)

type Human struct {
	name  string
	age   int
	phone string
}

// Human реализует fmt.Stringer
func (h Human) String() string {
	return "Имя:" + h.name + ", Возраст:" + strconv.Itoa(h.age) + " years, Контакт:" + h.phone
}

func main() {
	Bob := Human{"Боб", 39, "000-7777-XXX"}
	fmt.Println("Этот человек: ", Bob)
}

Возвращаясь к примеру с Box можно обнаружить, что Color также реализует интерфейс Stringer, поэтому у нас есть возможность изменить формат вывода информации. Если не реализовать этот интерфейс, fmt.Println выведет тип на печать в формате по умолчанию.

fmt.Println("Самая большая коробка: ", boxes.BiggestsColor().String())
fmt.Println("Самая большая коробка: ", boxes.BiggestsColor())

Внимание: Если тип реализует интерфейс error, fmt вызовет error(), поэтому в этом случае Вам не надо реализовывать Stringer.

Тип переменной в интерфейсе

Если тип переменной реализует интерфейс, то мы знаем, что значение любого типа, реализующего тот же самый интерфейс, может быть присвоено этой переменной. Но как мы узнаем, значение какого изначально типа присвоено этой переменной? Я покажу Вам два способа узнать это.

  • Подтверждение по шаблону запятая-ok

В Go существует синтаксис value, ok := element.(T). Так можно проверить, является ли переменная относящейся к указанному типу, где "value" - значение переменной, "ok" - это переменная булева типа, "element" - переменная типа interface и T - тип, который мы хотим подтвердить.

Если element является переменной типа, который мы указали, ok будет равен true, иначе - false.

Чтобы было понятнее, посмотрим на пример:

package main

import (
	"fmt"
	"strconv"
)

type Element interface{}
type List []Element

type Person struct {
	name string
	age  int
}

func (p Person) String() string {
	return "(Имя: " + p.name + " - возраст: 	" + strconv.Itoa(p.age) + " лет)"
}

func main() {
	list := make(List, 3)
	list[0] = 1       // целочисленный тип
	list[1] = "Привет" // строка
	list[2] = Person{"Деннис", 70}

	for index, element := range list {
		if value, ok := element.(int); ok {
			fmt.Printf("list[%d] - это целое число, его значение - %d\n", index, value)
		} else if value, ok := element.(string); ok {
			fmt.Printf("list[%d] - это строка, его значение - %s\n", index, value)
		} else if value, ok := element.(Person); ok {
			fmt.Printf("list[%d] - это Person, его значение %s\n", index, value)
		} else {
			fmt.Printf("list[%d] - это данные какого-то другого типа\n", index)
		}
	}
}

Пользоваться этим шаблоном довольно-таки просто, но если надо протестировать много типов, лучше воспользоваться switch.

  • тест с использованием switch

Давайте перепишем наш пример с использованием switch.

package main

import (
	"fmt"
	"strconv"
)

type Element interface{}
type List []Element

type Person struct {
	name string
	age  int
}

func (p Person) String() string {
	return "(Имя: " + p.name + " - возраст: " + strconv.Itoa(p.age) + " лет)"
}

func main() {
	list := make(List, 3)
	list[0] = 1       // целое число
	list[1] = "Hello" // строка
	list[2] = Person{"Деннис", 70}

	for index, element := range list {
		switch value := element.(type) {
		case int:
			fmt.Printf("list[%d] - целое число, его значение - %d\n", index, value)
		case string:
			fmt.Printf("list[%d] - строка, его значение - %s\n", index, value)
		case Person:
			fmt.Printf("list[%d] - Person, его значение - %s\n", index, value)
		default:
			fmt.Println("list[%d] - данные какого-то другого типа", index)
		}
	}
}

Нужно запомнить, что конструкция element.(type) не может быть использована вне тела switch, в этом случае надо использовать шаблон запятая-ok.

Встраиваемые интерфейсы

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

В исходном коде пакета container/heap мы можем видеть следующее определение:

type Interface interface {
    	sort.Interface // встраиваемый sort.Interface
    	Push(x interface{}) // метод Push для того, чтобы помещать элементы в кучу
    	Pop() interface{} // метод Pop, который изымает элементы из кучи
}

Мы видим, что sort.Interface является встраиваемым интерфейсом, поэтому в Interface неявно присутствуют три метода, содержащиеся внутри sort.Interface:

type Interface interface {
    	// Len - количество элементов в коллекции
    	Len() int
    	// Less определяет, надо ли перемещать элемент с индексом i
    	// перед элементом с индексом j.
    	Less(i, j int) bool
    	// Swap меняем местами элементы с индексами i и j.
    	Swap(i, j int)
}

Другой пример - io.ReadWriter из пакета io.

// io.ReadWriter
type ReadWriter interface {
		Reader
		Writer
}

Рефлексия

Рефлексия в Go используется для определения информации во время выполнения программы. Мы пользумеся пакетом reflect, и эта официальная статья объясняет, как reflect работает в Go.

В процессе использования reflect задействованы 3 шага. Во-первых, нужно конвертировать интерфейс в типы reflect (reflect.Type или reflect.Value в зависимости от ситуации).

t := reflect.TypeOf(i)    // получает мета-данные типа i в переменную t
v := reflect.ValueOf(i)   // получает значение типа i в переменную v

После этого мы может конвертировать типы, полученные в результате рефлексии, для того, чтобы получить нужные нам значения.

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("Тип:", v.Type())
fmt.Println("Вид является float64:", v.Kind() == reflect.Float64)
fmt.Println("Значение:", v.Float())

Наконец, если мы хотим изменить значения типов, полученных в результате рефлексии, нам нужно сделать их изменяемыми. Как было обсуждено ранее, есть разница между передачей по ссылке и по значению. Следующий код не скомпилируется:

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)

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

var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)

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

Ссылки