Пройти курс: Интерфейсы

В этом разделе поговорим об интерфейсах.

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

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

Здесь поведение — ключевой термин, который мы обсудим в ближайшее время.

Давайте рассмотрим пример, чтобы лучше понять это.

Одним из лучших примеров интерфейсов в реальном мире является розетка. Представьте, что нам нужно подключить к розетке различные устройства.

Давайте попробуем это реализовать. Вот типы устройств, которые мы будем использовать.

type mobile struct {
    brand string
}

type laptop struct {
    cpu string
}

type toaster struct {
    amount int
}

type kettle struct {
    quantity string
}

type socket struct{}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь определим метод Draw на типе, допустим, mobile. Здесь мы просто выведем свойства типа.

func (m mobile) Draw(power int) {
    fmt.Printf("%T -> brand: %s, power: %d", m, m.brand, power)
}
Вход в полноэкранный режим Выход из полноэкранного режима

Отлично, теперь мы определим метод Plug на типе socket, который принимает в качестве аргумента наш тип mobile.

func (socket) Plug(device mobile, power int) {
    device.Draw(power)
}
Вход в полноэкранный режим Выход из полноэкранного режима

Попробуем «подключить» или «подсоединить» тип mobile к нашему типу socket в функции main.

package main

import "fmt"

func main() {
    m := mobile{"Apple"}

    s := socket{}
    s.Plug(m, 10)
}
Вход в полноэкранный режим Выход из полноэкранного режима

Если мы запустим эту функцию, то увидим следующее.

$ go run main.go
main.mobile -> brand: Apple, power: 10
Вход в полноэкранный режим Выход из полноэкранного режима

Это интересно, но, допустим, теперь мы хотим подключить наш ноутбук.

package main

import "fmt"

func main() {
    m := mobile{"Apple"}
    l := laptop{"Intel i9"}

    s := socket{}

    s.Plug(m, 10)
    s.Plug(l, 50) // Error: cannot use l as mobile value in argument
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Как мы видим, это приведет к ошибке.

Что нам теперь делать? Определить другой метод? Например, PlugLaptop?

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

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

Мы можем просто определить интерфейс, например PowerDrawer, и использовать его в нашей функции Plug, чтобы разрешить любое устройство, которое удовлетворяет критериям, а именно: тип должен иметь метод Draw, соответствующий сигнатуре, которую требует интерфейс.

В любом случае, сокету не нужно ничего знать о нашем устройстве, и он может просто вызвать метод Draw.

Теперь давайте попробуем реализовать наш интерфейс PowerDrawer. Вот как это будет выглядеть.

Принято использовать «-er» в качестве суффикса в имени. И, как мы обсуждали ранее, интерфейс должен описывать только ожидаемое поведение. В нашем случае это метод Draw.

type PowerDrawer interface {
    Draw(power int)
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь нам нужно обновить наш метод Plug, чтобы он принимал в качестве аргумента устройство, реализующее интерфейс PowerDrawer.

func (socket) Plug(device PowerDrawer, power int) {
    device.Draw(power)
}
Вход в полноэкранный режим Выход из полноэкранного режима

А чтобы удовлетворить интерфейс, мы можем просто добавить методы Draw ко всем типам устройств.

type mobile struct {
    brand string
}

func (m mobile) Draw(power int) {
    fmt.Printf("%T -> brand: %s, power: %dn", m, m.brand, power)
}

type laptop struct {
    cpu string
}

func (l laptop) Draw(power int) {
    fmt.Printf("%T -> cpu: %s, power: %dn", l, l.cpu, power)
}

type toaster struct {
    amount int
}

func (t toaster) Draw(power int) {
    fmt.Printf("%T -> amount: %d, power: %dn", t, t.amount, power)
}

type kettle struct {
    quantity string
}

func (k kettle) Draw(power int) {
    fmt.Printf("%T -> quantity: %s, power: %dn", k, k.quantity, power)
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем подключить все наши устройства к сокету с помощью нашего интерфейса!

func main() {
    m := mobile{"Apple"}
    l := laptop{"Intel i9"}
    t := toaster{4}
    k := kettle{"50%"}

    s := socket{}

    s.Plug(m, 10)
    s.Plug(l, 50)
    s.Plug(t, 30)
    s.Plug(k, 25)
}
Вход в полноэкранный режим Выход из полноэкранного режима

И, как мы и ожидали, все работает.

$ go run main.go
main.mobile -> brand: Apple, power: 10
main.laptop -> cpu: Intel i9, power: 50
main.toaster -> amount: 4, power: 30
main.kettle -> quantity: Half Empty, power: 25
Вход в полноэкранный режим Выход из полноэкранного режима

Но почему это считается такой мощной концепцией?

Ну, интерфейс может помочь нам разделить наши типы. Например, поскольку у нас есть интерфейс, нам не нужно обновлять нашу реализацию socket. Мы можем просто определить новый тип устройства с помощью метода Draw.

В отличие от других языков, интерфейсы Go реализуются неявно, поэтому нам не нужно что-то вроде ключевого слова implements. Это означает, что тип удовлетворяет интерфейсу автоматически, если у него есть «все методы» интерфейса.

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

Далее поговорим о пустом интерфейсе. Пустой интерфейс может принимать значение любого типа.

Вот как мы его объявляем.

var x interface{}
Войти в полноэкранный режим Выход из полноэкранного режима

Но зачем он нам нужен?

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

Вот несколько примеров:

  • Чтение разнородных данных из API.
  • Переменные неизвестного типа, как в функции fmt.Prinln.

Чтобы использовать значение типа empty interface{}, мы можем использовать утверждение типа или переключатель типов для определения типа значения.

Утверждение типа

Утверждение типа предоставляет доступ к интерфейсному значению, лежащему в основе конкретного значения.

Например:

func main() {
    var i interface{} = "hello"

    s := i.(string)
    fmt.Println(s)
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Этот оператор утверждает, что значение интерфейса имеет конкретный тип, и присваивает переменной значение базового типа.

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

Утверждение типа может возвращать два значения:

  • Первое — базовое значение.
  • Второе — булево значение, которое сообщает, удалось ли утверждение.
s, ok := i.(string)
fmt.Println(s, ok)
Вход в полноэкранный режим Выход из полноэкранного режима

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

В некотором смысле это похоже на то, как мы читаем значения из карты.

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

f, ok := i.(float64)
fmt.Println(f, ok)
Вход в полноэкранный режим Выход из полноэкранного режима

Но если интерфейс не содержит тип, то оператор вызовет панику.

f = i.(float64)
fmt.Println(f) // Panic!
Войти в полноэкранный режим Выйти из полноэкранного режима
$ go run main.go
hello
hello true
0 false
panic: interface conversion: interface {} is string, not float64
Войти в полноэкранный режим Выход из полноэкранного режима

Переключатель типов

Здесь оператор switch может быть использован для определения типа переменной типа empty interface{}.

var t interface{}
t = "hello"

switch t := t.(type) {
case string:
    fmt.Printf("string: %sn", t)
case bool:
    fmt.Printf("boolean: %vn", t)
case int:
    fmt.Printf("integer: %dn", t)
default:
    fmt.Printf("unexpected: %Tn", t)
}
Войти в полноэкранный режим Выход из полноэкранного режима

Если мы выполним это, то сможем убедиться, что у нас есть тип string.

$ go run main.go
string: hello
Войти в полноэкранный режим Выход из полноэкранного режима

Свойства

Давайте обсудим некоторые свойства интерфейсов.

Нулевое значение

Нулевым значением интерфейса является nil.

package main

import "fmt"

type MyInterface interface {
    Method()
}

func main() {
    var i MyInterface

    fmt.Println(i) // Output: <nil>
}
Вход в полноэкранный режим Выход из полноэкранного режима

Встраивание

Мы можем встраивать интерфейсы подобно структурам.

Например

type interface1 interface {
    Method1()
}

type interface2 interface {
    Method2()
}

type interface3 interface {
    interface1
    interface2
}
Войти в полноэкранный режим Выход из полноэкранного режима

Значения

Значения интерфейсов сопоставимы.

package main

import "fmt"

type MyInterface interface {
    Method()
}

type MyType struct{}

func (MyType) Method() {}

func main() {
    t := MyType{}
    var i MyInterface = MyType{}

    fmt.Println(t == i)
}
Войти в полноэкранный режим Выход из полноэкранного режима

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

Под капотом значение интерфейса можно представить как кортеж, состоящий из значения и конкретного типа.

package main

import "fmt"

type MyInterface interface {
    Method()
}

type MyType struct {
    property int
}

func (MyType) Method() {}

func main() {
    var i MyInterface

    i = MyType{10}

    fmt.Printf("(%v, %T)n", i, i) // Output: ({10}, main.MyType)
}
Вход в полноэкранный режим Выход из полноэкранного режима

На этом мы рассмотрели интерфейсы в Go.

Это действительно мощная функция, но помните: «Чем больше интерфейс, тем слабее абстракция» — Роб Пайк.


Эта статья является частью моего открытого курса по Go, доступного на Github.

karanpratapsingh / go-course

Освойте основы и расширенные возможности языка программирования Go

Курс по Go

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

Этот курс также доступен на моем сайте, а также на Educative.io

Оглавление

  • Начало работы

    • Что такое Go?
    • Зачем изучать Go?
    • Установка и настройка
  • Глава I

    • Hello World
    • Переменные и типы данных
    • Форматирование строк
    • Управление потоком данных
    • Функции
    • Модули
    • Пакеты
    • Рабочие пространства
    • Полезные команды
    • Сборка
  • Глава II

    • Указатели
    • Структуры
    • Методы
    • Массивы и фрагменты
    • Карты
  • Глава III

    • Интерфейсы
    • Ошибки
    • Паника и восстановление
    • Тестирование
    • Дженерики
  • Глава IV

    • Параллелизм
    • Гороутины
    • Каналы
    • Выбрать
    • Пакет синхронизации
    • Расширенные шаблоны параллелизма
    • Контекст
  • Приложение

    • Следующие шаги
    • Ссылки

Что такое Go?

Go (также известный как Golang) — это язык программирования, разработанный в Google в 2007 году и открытый в 2009 году.

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

Посмотреть на GitHub

Оцените статью
devanswers.ru
Добавить комментарий