После изучения данного урока вы сможете:
- Понять, о чем могут сообщить типы;
- Открыть для себя интерфейсы по ходу реализации кода;
- Разобраться в интерфейсах стандартной библиотеки;
- Спасти человечество во время марсианского вторжения.
Ручка и бумага — не единственные инструменты, которые можно использовать для записи идей. Карандаш и салфетка также подойдут. Мелки, маркеры и механические карандаши также можно использовать для записи пометки в блокноте, слогана на листе ватмана или записи в журнале. Процесс записи довольно гибок.
Рекомендуем вам супер TELEGRAM канал по Golang где собраны все материалы для качественного изучения языка. Удивите всех своими знаниями на собеседовании! 😎
Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.
Содержание статьи
- Тип interface в Golang
- Примеры использования интерфейсов в коде на Golang
- Удовлетворение требований интерфейса в Go
В стандартной библиотеке Go есть интерфейс для записи. Он называется Writer
, с его помощью можно записывать текст, картинки, разделенные запятыми значения (CSV), сжатые архивы и многое другое. Можно выполнить запись вывода на экране, файла на диск или ответ на веб-запрос. С помощью единого интерфейса, Go может записать все что угодно, заняв любое количество памяти. Writer
очень гибок.
Шариковая ручка 0,5 мм с синими чернилами — это конкретный объект, тогда как пишущий инструмент — не совсем четкая идея. С помощью интерфейсов код может выражать такие абстрактные понятия, как пишущий инструмент. Подумайте о том, что именно можно сделать, а не о том, что оно из себя представляет. Данный способ мышления через интерфейсы поможет коду адаптироваться к изменениям.
Осмотритесь вокруг, какие конкретные вещи находятся рядом? Что с ними можно сделать? Можно ли сделать то же самое с другими объектами? Есть ли у них общее поведение или интерфейс?
Тип interface в Golang
Большинство типов фокусируется на значениях, что они хранят: integer для целых чисел, string для текста и так далее. Тип interface
отличается. Интерфейсы сконцентрированы на том, что тип делает, а не на сохраняемых им значениях.
Методы выражают поведение предоставленного типа, поэтому интерфейсы объявляются с набором методов, которых тип должен удовлетворить. В следующем примере объявляется переменная с типом interface
:
1 2 3 |
var t interface { talk() string } |
Переменная t
может хранить значение любого типа, что удовлетворяет интерфейсу. Говоря точнее, тип подойдет интерфейсу, если он объявляет метод под названием talk
, что не принимает аргументы и возвращает строку.
В следующем примере объявляются два типа, что соответствуют требованиям:
1 2 3 4 5 6 7 8 9 10 11 |
type martian struct{} func (m martian) talk() string { return "nack nack" } type laser int func (l laser) talk() string { return strings.Repeat("pew ", int(l)) } |
Хотя martian
является структурой без полей и laser
является целым числом, оба типа предоставляют метод talk
, следовательно могут быть присвоены к t
, как показано в следующем примере:
1 2 3 4 5 6 7 8 9 |
var t interface { talk() string } t = martian{} fmt.Println(t.talk()) // Выводит: nack nack t = laser(3) fmt.Println(t.talk()) // Выводит: pew pew pew |
Изменяемая переменная t
способна принимать форму martian
или laser
. Программисты говорят, что интерфейсы обладают полиморфизмом, то есть возможность менять форму.
На заметку: В отличие от Java, в Go
martian
иlaser
напрямую не объявляют, что они имплементируют интерфейс. О преимуществах поговорим далее в уроке.
Обычно интерфейсы объявляются как именованные типы, что можно повторно использовать. Существуют правило в отношении именования типов интерфейса с суффиком -er
: к примеру, talker, то есть тот, кто говорит (talk+er), как показано в следующем примере:
1 2 3 |
type talker interface { talk() string } |
Тип интерфейса можно использовать везде, где используются другие типы. К примеру, следующая функция shout
обладает параметром типа talker
.
1 2 3 4 |
func shout(t talker) { louder := strings.ToUpper(t.talk()) fmt.Println(louder) } |
Вы можете использовать функцию shout
с любым значением, что удовлетворяет требования интерфейса talker
, будь то martian
или laser
, как показано в следующем примере.
1 2 |
shout(martian{}) // Выводит: NACK NACK shout(laser(2)) // Выводит: PEW PEW |
Аргумент, который вы передаете функции shout
, должен удовлетворять требования интерфейса talker
. К примеру, тип crater
не удовлетворяет интерфейс talker
, поэтому компилятор Go откажется компилировать программу:
1 2 |
type crater struct{} shout(crater{}) // crater не имплементирует talker (отсутствует метод talk) |
Интерфейсы показывают свою гибкость, когда вам нужно изменить или расширить код. При объявлении нового типа с методом talk
функция shout
будет с ним работать. Любой код, что зависит только от интерфейса, может оставаться прежним, даже если имплементации добавляются или меняются.
Стоит отметить, что интерфейсы могут использоваться со встраиванием структуры, особенностью языка, что была описана в статье о композициях и встраивании. К примеру, в следующем коде laser
встраивается в starship
.
1 2 3 4 5 6 7 8 |
type starship struct { laser } s := starship{laser(3)} fmt.Println(s.talk()) // Выводит: pew pew pew shout(s) // Выводит: PEW PEW PEW |
Когда говорит космический корабль, за него говорит лазер. Внедрение laser
предоставляет структуре starship
метод talk
, что встраивается в laser
. Теперь космический корабль starship
также удовлетворяет требования интерфейса talker
, позволяя ему использоваться вместе с shout
.
Используемые вместе композиции и интерфейсы создают очень мощный инструмент проектирования.
Билл Веннерс, JavaWorld
Вопросы для проверки:
- Измените метод
talk
лазера из Листинга 4 для предотвращения стрельбы из марсианского оружия, чтобы можно было спасти человечество во время вторжения. - Расширьте Листинг 4, 5 и 6, объявив новый тип
rover
(структура) c методомtalk
, что возвращает «whir whir». Используйте функциюshout
с вашим новым типом.
Примеры использования интерфейсов в коде на Golang
Вместе с Go вы можете начать имплементировать код и познавать интерфейсы по ходу дела. Любой код имплементирует интерфейс, даже тот код, что уже существует. Разберем тему на примере.
В следующем листинге выводится выдуманная дата, что состоит из дня года и часа дня.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package main import ( "fmt" "time" ) // stardate возвращает выдуманное измерение времени для указанной даты. func stardate(t time.Time) float64 { doy := float64(t.YearDay()) h := float64(t.Hour()) / 24.0 return 1000 + doy + h } func main() { day := time.Date(2012, 8, 6, 5, 17, 0, 0, time.UTC) fmt.Printf("%.1f Curiosity has landed\n", stardate(day)) // Выводит: 1219.2 Curiosity has landed } |
Функция stardate
из Листинга 8 ограничивается земными датами. Чтобы исправить это, следующий листинг объявляет интерфейс, который может использовать stardate
но уже по своему усмотрению:
1 2 3 4 5 6 7 8 9 10 11 |
type stardater interface { YearDay() int Hour() int } // stardate возвращает выдуманное измерение времени. func stardate(t stardater) float64 { doy := float64(t.YearDay()) h := float64(t.Hour()) / 24.0 return 1000 + doy + h } |
Новая функция stardate
в Листинге 9 продолжает оперировать земными датами, потому что тип time.Time
из стандартной библиотеке удовлетворяет требования интерфейса stardater
. Интерфейсы в Go являются удовлетворяемыми косвенным образом, что особенно полезно при работе с чужим кодом.
На заметку: В языках вроде Java это использовать не получилось бы, потому что для
java.time
понадобилось бы в открытую указать наimplements stardater
.
С находящимся на месте интерфейсом stardater
Листинг 9 может быть расширен с типом sol
, что удовлетворяет требования интерфейса с методами YearDay
и Hour
, как показано в следующем коде.
1 2 3 4 5 6 7 8 9 |
type sol int func (s sol) YearDay() int { return int(s % 668) // Марсианской год состоит из 668 дней } func (s sol) Hour() int { return 0 // Неизвестный час } |
Функция stardate
оперирует как земными датами, так и марсианскими днями, как показано в следующем листинге.
1 2 3 4 5 |
day := time.Date(2012, 8, 6, 5, 17, 0, 0, time.UTC) fmt.Printf("%.1f Curiosity has landed\n", stardate(day)) // Выводит: 1219.2 Curiosity has landed s := sol(1422) fmt.Printf("%.1f Happy birthday\n", stardate(s)) // Выводит: 1086.0 Happy birthday |
Вопрос для проверки:
В чем преимущество неявно удовлетворяемых интерфейсов?
Удовлетворение требований интерфейса в Go
Стандартная библиотека Go экспортирует некоторое число интерфейсов с одним методом, которых можно имплементировать в коде.
Go предпочитает композицию, а не наследование, используя простые интерфейсы с одним методом… Это простые и понятные границы между компонентами.
Роб Пайк,
Go от Google: Проектирование языка в
Службе Программной Инженерии
К примеру, пакет fmt
объявляет интерфейс Stringer
следующим образом:
1 2 3 |
type Stringer interface { String() string } |
Если тип предоставляет метод String
то Println
, Sprintf
и другие функции отображения содержимого будут используют его. В следующем листинге метод String
нужен для контроля над тем, как пакет fmt
отображает место расположения.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package main import "fmt" // location с широтой и долготой в десятичных градусах type location struct { lat, long float64 } // String форматирует location с широтой и долготой func (l location) String() string { return fmt.Sprintf("%v, %v", l.lat, l.long) } func main() { curiosity := location{-4.5895, 137.4417} fmt.Println(curiosity) // Выводит: -4.5895, 137.4417 } |
Вдобавок к fmt.Stringer
популярные интерфейсы стандартной библиотеки включают io.Reader
, io.Writer
и json.Marshaler.
Интерфейс io.ReadWriter
предоставляет пример внедрения интерфейса, что похож на встраивание структуры. Однако, в отличие от структуры, у интерфейсов нет полей или прикрепленных методов, поэтому встраивание интерфейса может сэкономит время, в противном случае затрачиваемое на набор текста.
Вопрос для проверки:
Напишите метод String для типа coordinate
и location
и используйте его для отображения координат в более читабельном формате.
1 2 3 4 |
type coordinate struct { d, m, s float64 h rune } |
Ваша программа должна выводить: Elysium Planitia is at 4°30'0.0" N, 135°54'0.0" E
Заключение
- Типы интерфейса уточняют запрашиваемое поведение с помощью набора методов;
- Интерфейсы удовлетворяются косвенным образом новым или уже существующим кодом в любом пакете;
- Структура удовлетворяет интерфейс, которая удовлетворяют внедренные типы;
- Следуйте примеру стандартной библиотеки и стремитесь сохранить интерфейсы небольшими.
Итоговое задание для проверки:
Напишите программу, что выводит координаты в формате JSON, расширяя работу, сделанную для предварительной быстрой проверки. JSON вывод должен предоставить все координаты в десятичных градусах (DD), а также в градусах, минутах и секундах:
1 2 3 4 5 6 7 8 |
{ "decimal": 135.9, "dms": "135°54'0.0\" E", "degrees": 135, "minutes": 54, "seconds": 0, "hemisphere": "E" } |
Этого можно добиться без модификации структуры координат через удовлетворение интерфейса json.Marshaler
для настройки JSON. Написанный вами метод MarshalJSON
может использовать json.Marshal
.
Обратите внимание, что для вычислений в десятичных градусах вам понадобится метод decimal, обсуждаемый в уроке о структурах и методах.
Администрирую данный сайт с целью распространения как можно большего объема обучающего материала для языка программирования Go. В IT с 2008 года, с тех пор изучаю и применяю интересующие меня технологии. Проявляю огромный интерес к машинному обучению и анализу данных.
E-mail: vasile.buldumac@ati.utm.md
Образование
Технический Университет Молдовы (utm.md), Факультет Вычислительной Техники, Информатики и Микроэлектроники
- 2014 — 2018 Universitatea Tehnică a Moldovei, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Universitatea Tehnică a Moldovei, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»
спасибо