В этом разделе поговорим об интерфейсах.
Что такое интерфейс?
Итак, интерфейс в 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 году.
Основное внимание в нем уделяется простоте, надежности и эффективности. Он был разработан, чтобы объединить эффективность, скорость и безопасность статически типизированного и компилируемого языка с легкостью…