#17 golang Интерфейсы - проверка типа type switch

Через интерфейсы в Go реализован полиморфизм.

В отличие от таких языков, как C++, Java и PHP, где типизация всегда явная, в Go при помощи интерфейсов реализована так называемая «утиная типизация».

Что это значит:

  • В C++, когда создаете класс, вы явно указываете, что он реализует такой интерфейс: «Вот мои документы, вот я наследовался от этого интерфейса, я его реализую. Я — утка. Я точно знаю, что я — утка».
  • В Go, когда вы создаете структуру с методами, она не знает, какому интерфейсу соответствует, нет явного механизма указания, к какому интерфейсу относится тип, вместо этого тут применяется "утиная типизация"

Пример 1 - пример проверки структуры на соответствие интерфейсу

Посмотрим, как это работает в коде. Демонстрация определения интерфейса:

type Payer interface {
   pay(int) error // сигнатура метода "оплатить"
 }

-- в этом фрагменте кода объявлен интерфейс Payer («плательщик»).
Чтобы соответствовать этому интерфейсу, нужно иметь метод pay («оплатить») который принимает int и возвращает ошибку.

Далее реализуем структуру «кошелёк», у которой есть поле cash, то есть количество денег в этом кошельке, и есть метод «оплатить»:

// структура, дальше мы навесим на нее метода
// и она станет соответствать интерфейсу выше
type Wallet struct {
	cash int // количество денег
}

// навещиваем метод на структуру
func (w *Wallet) pay(amount int) error {
	if w.cash < amount {
		return fmt.Errorf("не хватает денег в кошельке")
	}
	w.cash -= amount
	return nil
}

Обратите внимание: В реализации кошелька нигде нет упоминания того, что он каким-либо образом реализует интерфейс «плательщик», дальше golang "по факту" (утиной типизацией) будет проверять, есть ли соответствие или нет.

Теперь давайте используем всё это в коде:

// наш интерфейс
type Payer interface {
	pay(int) error // сигнатура метода "оплатить"
}

// структура, дальше мы навесим на нее метода
// и она станет соответствать интерфейсу выше
type Wallet struct {
	cash int // количество денег
}

// навещиваем метод на структуру
func (w *Wallet) pay(amount int) error {
	if w.cash < amount {
		return fmt.Errorf("не хватает денег в кошельке")
	}
	w.cash -= amount
	return nil
}

func main() {
	// Создадим кошелек
	VasyaWallet := &Wallet{
		cash: 100,
	}
	// и купим с него что-то на 10 монет
	buyIt(VasyaWallet, 10)
	fmt.Println("Осталось монет:", VasyaWallet.cash)
}

// Этот метод требует интерфейс "платильщик"
func buyIt(payer Payer, amount int) {
	err := payer.pay(amount)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Спасибо за покупку через %T\n\n", payer)
}

-- тут мы использовали %T для вывод типа аргумента. Если запустим эту программу, то получим распечатку:

Спасибо за покупку через *main.Wallet

Осталось монет: 90

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

Пример 2 - несколько разных структур, реализуют один интерфейс

// Обший интерфес "Платильщик"
type Payer interface {
	pay(int) error   // что-то оплачиваем
	getBalance() int // узнаем сколько денег еще осталось
}

type Wallet struct {
	cash int
}

// навешиваем метод на структуру
func (w *Wallet) pay(amount int) error {
	if w.cash < amount {
		return fmt.Errorf("не хватает денег в кошельке")
	}
	w.cash -= amount
	return nil
}

func (w *Wallet) getBalance() int {
	return w.cash
}

// Структура описывающая банковскую карточку
type Card struct {
	Balance    int
	ValidUntil string
	Cardholder string
	CVV        string
	Number     string
}

func (c *Card) pay(amount int) error {
	if c.Balance < amount {
		return fmt.Errorf("не хватает денег на карте")
	}
	c.Balance -= amount
	return nil
}

func (c *Card) getBalance() int {
	return c.Balance
}

// Платежные аккаунт в какой-то системе
type KtuPay struct {
	money int
	ktuId string
}

func (a *KtuPay) pay(amount int) error {
	if a.money < amount {
		return fmt.Errorf("не хватает денег на аккаунте")
	}
	a.money -= amount
	return nil
}

func (kp *KtuPay) getBalance() int {
	return kp.money
}

// Функция для покупки чего-то через любое средство оплаты,
// но оно должно соответствовать интерфейсу Payer
func Buy(p Payer, amount int) {
	err := p.pay(amount)
	if err != nil {
		fmt.Printf("Ошибка при оплате %T: %v\n", p, err)
		return
	}
	fmt.Printf("Спасибо за покупку через %T\n", p)
}

// Произведем оплату разными средствами
func main() {
	myWallet := &Wallet{cash: 100}
	Buy(myWallet, 10)
	fmt.Println("Осталось монет:", myWallet.getBalance())

	var myMoney Payer
	myMoney = &Card{Balance: 80, Cardholder: "rvasily"}
	Buy(myMoney, 10)
	fmt.Println("Осталось монет:", myMoney.getBalance())

	myMoney = &KtuPay{money: 70}
	Buy(myMoney, 10)
	fmt.Println("Осталось монет:", myMoney.getBalance())

}

-- мы описали три разных платежных средства, которые, тем не менее, соответствуют одному интерфейсу Payer. Если запустить код, то получим распечатку вроде:

Спасибо за покупку через *main.Wallet
Осталось монет: 90
Спасибо за покупку через *main.Card
Осталось монет: 70
Спасибо за покупку через *main.KtuPay
Осталось монет: 60

Определение типа, скрывающегося под интерфейсом - явная проверка на соответствие, получение типа из интерфейса - type switch

Пусть у нас есть переменная какого-то типа, которая объявлена в функции как соответсвующая интерфейсу.

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

switch переменнаяТипаИнтерфейс.(type)


Добавим функцию Buy2() с использованием type switch:

// Обший интерфес "Платильщик"
type Payer interface {
	pay(int) error   // что-то оплачиваем
	getBalance() int // узнаем сколько денег еще осталось
}

type Wallet struct {
	cash int
}

// навешиваем метод на структуру
func (w *Wallet) pay(amount int) error {
	if w.cash < amount {
		return fmt.Errorf("не хватает денег в кошельке")
	}
	w.cash -= amount
	return nil
}

func (w *Wallet) getBalance() int {
	return w.cash
}

// Структура описывающая банковскую карточку
type Card struct {
	Balance    int
	ValidUntil string
	Cardholder string
	CVV        string
	Number     string
}

func (c *Card) pay(amount int) error {
	if c.Balance < amount {
		return fmt.Errorf("не хватает денег на карте")
	}
	c.Balance -= amount
	return nil
}

func (c *Card) getBalance() int {
	return c.Balance
}

// Платежные аккаунт в какой-то системе
type KtuPay struct {
	money int
	ktuId string
}

func (a *KtuPay) pay(amount int) error {
	if a.money < amount {
		return fmt.Errorf("не хватает денег на аккаунте")
	}
	a.money -= amount
	return nil
}

func (kp *KtuPay) getBalance() int {
	return kp.money
}

// Покупка с проверкой типа платежного средства
func Buy2(p Payer, amount int) {
	switch p.(type) {
	case *Wallet:
		fmt.Println("Оплата наличными")
	case *Card:
		fmt.Println("Платим карточкой!")
	default:
		fmt.Println("Новая платежная система!")
	}

	err := p.pay(amount)
	if err != nil {
		fmt.Printf("Ошибка при оплате %T: %v\n", p, err)
		return
	}
	fmt.Printf("Спасибо за покупку через %T\n", p)
}

// Произведем оплату разными средствами
func main() {
	myWallet := &Wallet{cash: 100}
	Buy2(myWallet, 10)
	fmt.Println("Осталось монет:", myWallet.getBalance())

	var myMoney Payer
	myMoney = &Card{Balance: 80, Cardholder: "rvasily"}
	Buy2(myMoney, 10)
	fmt.Println("Осталось монет:", myMoney.getBalance())

	myMoney = &KtuPay{money: 70}
	Buy2(myMoney, 10)
	fmt.Println("Осталось монет:", myMoney.getBalance())
}

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

Обращение к полям структуры, в случае если аргумент передан как интерфейс

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


type Wallet struct {
	cash int
}

// навешиваем метод на структуру
func (w *Wallet) pay(amount int) error {
	if w.cash < amount {
		return fmt.Errorf("не хватает денег в кошельке")
	}
	w.cash -= amount
	return nil
}

// этот метод не будет входить в интерфейс
func (w *Wallet) sayPrivet() {
	fmt.Println("Привет!")
}

// Обший интерфес "Платильщик"
type Payer interface {
	pay(int) error // что-то оплачиваем
}

// Обращение к полю и методу, не описываемым интерфейсом
func showCash(p Payer) {
	// p.sayPrivet() // не сработает!
	// p.cash // не сработает!
	wallet, ok := p.(*Wallet) // пробуем преобразовать тип
	if ok {
		fmt.Println("Осталось монет", wallet.cash)
		wallet.sayPrivet()
	} else {
		fmt.Println("Кажется, это не обычный кошелек, а что-то ещё")
	}
}

func main() {
	var x Payer = &Wallet{cash: 10}
	showCash(x)
}

-- тут стоит заметить, что поля/переменные (в отличии от методов) мы даже при наличии желания не сможем описать в интерфейсе (golang не поддерживает это в принципе), а потому для получения доступа к полям, как в ситуации выше, нам остается только явно преобразовывать тип через конструкцию вида:

 wallet, ok := p.(*ТипДанных)

Видео-материалы

  • : ВкВидео | Ютуб | Телеграм

Key Words for FKN + antitotal forum (CS VSU):

vedro-compota's picture

уточнить почему, если ожидаается интерфейс, то передается указатель, а не значение

_____________
матфак вгу и остальная классика =)