Структуры данных
Last updated
Was this helpful?
Last updated
Was this helpful?
Массив (array) - это пронумерованная последовательность элементов одного типа, называемая типом элемента (element type). Количество элементов называется длиной массива и никогда не бывает отрицательным.
Длина является частью типа массива; она должна вычисляться как неотрицательная константа, представимая значением типа int. Длина массива a может быть определена с помощью встроенной функции len. Элементы могут быть адресованы целочисленными индексами от 0 до len(a)-1. Типы массивов всегда одномерны, но могут быть составлены для формирования многомерных типов.
Массив – это коллекция фиксированного размера. Акцент здесь ставится именно на фиксированный размер, поскольку, как только вы зададите длину массива, позже вы уже не сможете ее изменить. Давайте рассмотрим пример. Мы создадим массив из четырех целых значений:
Длина и тип
В примере выше переменная arr
определена как массив типа [4]int
, это означает, что массив состоит из четырех элементов. Важно обратить внимание на то, что размер 4
включен в определение типа.
Из этого исходит, что на самом деле массивы разной длины — это массивы разных типов. Вы не сможете приравнять друг к другу массивы разной длины и не сможете присвоить значение одного массива другому в таком случае:
Представление в памяти
Массив хранится в виде последовательности из n
блоков определенного типа:
Эта память распределяется в момент, когда вы инициализируете переменную типа массив.
Передача по ссылке В Go нет такой вещи, как передача по ссылке, вместо этого все передается по значению. Если присвоить значение массива другой переменной, то присваиваемое значение просто будет скопировано. Если вы хотите передать лишь «ссылку» на массив, используйте указатели: При распределении памяти и в функции массив на самом деле является простым типом данных и работает во многом аналогично структурам.
Пустой массив - все элементы равны zero-value type.
Придобавлении элемента в масссив свыше его размера (при ин циализации) или при обращении к индексу, который выходит за пределы длинны массива, компилятор будет ругаться.
Если в литерале массива на месте длинны находится троеточие “ . . . ”, то длинна массива определяется количеством инициализаторов.
Массивы имеют право на существование, но они немного негибкие, поэтому вы не увидите их так часто в коде на Go. Однако слайсы есть везде. Они построены на основе массивов, предоставляя большие возможности и удобство.
Спецификация типа для слайсов это []T
, где T
- это тип элементов. В отличие от массивов, в тип слайсов длина не входит.
Литерал слайса объявляется как и у массивов, но без указания количества элементов:
Слайс можно создать с помощью встроенной функции make
, которая имеет такую сигнатуру:
где T
- это тип элементов создаваемого слайса. Функция make
принимает следующие аргументы: тип, длину и опционально вместимость (capacity). Во время вызова функция make
создаёт массив и возвращает слайс, который указывает на него.
Если не указать вместимость, по умолчанию она равна указанной длине. Вот более ёмкая версия того же кода:
Длина и вместимость слайса могут быть получены с помощью встроенных функций:
В следующих двух секциях обсудим отношение между длиной и вместимостью
Нулевое значение слайса это nil
. Функции len
и cap
возвращают 0 для нулевого слайса.
Слайс можно также создать “слайсингом” существующего слайса или массива. Слайсинг осуществляется с помощью указания полуоткрытого промежутка с двумя индексами, разделёнными двоеточием. Например, выражение b[1:4]
создаст слайс, включающий элементы с 1 по 3 из b
(индексы полученного слайса будут от 0 до 2).
Начальный и конечный индексы в промежутки указывать необязательно; по умолчанию они равны нулю, как и длина слайса:
Этот синтаксис подходит и для создания слайса данного массива:
Подробнее о слайсах
Слайс - это дескриптор сегмента массива. Он состоит из указателя на массив, длины сегмента и его вместимости (максимальной длины сегмента).
Наша переменная s
, созданная ранее с помощью make([]byte, 5)
, имеет такую структуру:
Длина - это число элементов, на которое ссылается слайс. Вместимость - это число элементов лежащего в основе массива (начиная с элемента, на который ссылается указатель слайса). Разница между длиной и вместимостью станет чётче по ходу знакомства с остальными примерами.
По мере изменения промежутков слайса, можно наблюдать изменения в структуре данных слайса и их отношениях с лежащим в основе массивом:
Слайсниг не производит копирование данных слайса. Создаётся новое значение слайса, указывающее на исходный массив. Это делает операцию слайсинга такой же эффективной, как и манипуляции с индексами массива. Таким образом, изменение элементов (не самого слайса) нового слайса изменяет элементы исходного:
Ранее мы слайсили s
до длины, меньшей, чем вместимость. Мы можем увеличить s
до её вместимости, сделав слайсинг снова:
Слайс нельзя сделать большим, чем его вместимость. Если вы попытаетесь, это вызовет панику времени выполнения, как и когда происходит обращение к индексу вне границ слайса или массива.
Увеличение слайсов (функции copy и append)
Для увеличения вместимости слайса необходимо создать новый, более крупный слайс и скопировать элементы исходного слайса в него. Эта техника показывает, как реализуются динамические массивы в других языках. Следующий пример удваивает вместимость s
, создавая новый слайс t
, копируя содержимое s
в t
, а затем присваивая s
значение слайса t
:
Повторяющаяся часть этой часто используемой операции реализована с помощью простой встроенной функции copy
. Как подсказывает её имя, эта функция копирует данные из слайса-источника в слайс-приёмник. Возвращается количество скопированных элементов.
Функция copy
поддерживает копирование между слайсами разной длины (она скопирует только до меньшего числа элементов). К тому же, copy
может справиться со слайсами, относящимися к одному массиву в основе этих слайсов, работая правильно с перекрытием слайсов.
Используя copy
, можно упростить кусочек кода выше:
Часто необходимо добавить данные в конец слайса. Эта функция добавляет элементы в байтовый слайс, увеличивая сам слайс по необходимости, и возвращает обновлённый слайс:
Можно было бы использовать AppendByte таким образом:
Такие функции, как AppendByte, полезны, потому что они предоставляют полный контроль над способом увеличения слайсов. В зависимости от характеристики программы может понадобиться создание более маленького или большого слайса, или загрузить слайс элементами до предельного размера памяти.
Хотя большинству программ не нужен абсолютный контроль, поэтому Go предоставляет встроенную функцию append
, которая хорошо подходит в большинстве случаев. Она имеет такую сигнатуру:
Эта функция добавляет элементы в конец слайса s
и увеличивает вместимость, если нужно.
Чтобы добавить один слайс в другой, используйте …
в качестве второго аргумента, чтобы он стал списком аргументов.
Так как нулевой слайс работает как слайс нулевой длины, вы можете объявить переменную со слайсом и затем циклично добавлять в неё элементы:
Возможная ловушка
Как говорилось ранее, переслайсинг (re-slicing) среза не создаёт копию массива в основании. Массив полностью будет существовать в памяти, пока на него не перестанут ссылаться. Иногда это вызывает хранение всех данных в памяти, когда нужна только их небольшая часть.
Например, функция FindDigits
загружает файл в память и ищет в нём первую группу последовательных цифр, возвращая их в новом слайсе.
Этот код работает, как и говорилось, однако возвращаемый срез []byte
указывает на массив, содержащий файл целиком. Так как слайс ссылается на исходный массив, пока слайс есть в памяти, сборщик мусора не сможет очистить массив; несколько важных байтов файла держат всё содержимое в памяти.
Чтобы исправить это, можно скопировать интересующие нас данные в новый слайс до того, как вернуть значение.
Более краткая версия этой функции могла быть реализована с помощью append
.
Иногда бывает нужно скопировать один слайс в другой: не со- здать ссылку на ту же область памяти, а именно честно скопировать. Такой способ неправильный.
Когда вы создаете пустой слайс и вызываете функцию копии, она копирует элементы в уже существующий слайс, уже в занятые элементы, то есть на место тех элементов, количество которых нам показывает переменная len. В данном случае len для слайса, в который мы копируем, равна 0, и мы на самом деле ничего не скопируем. Для того чтобы скопировать полноценно, нам нужно создать новый слайс, такой же размерности и такой же длины, сразу с данными, и уже скопировать в него.
Тогда поведение будет ожидаемым. Копировать можно не только в переменную, но, например, в срез, слайс, который вы только что получили, который ссылается на какую-то другую область памяти, то есть на другой слайс, на часть другого слайса. Это очень полезный трюк, когда вам нужно хранить сначала записать длину данных, а потом сами данные, при этом размер данных вы еще не знаете (например, при бинарной упаковке). Поэтому вы пишете сначала нулевой размер, потом пишете данные, а потом, используя вот такой трюк, вы пишете длину в нужное место.
Map, он же хеш-таблица, он же ассоциативный массив, он же - сложное слово - «отображение». map позволяет по ключу быстро получить значение. Это очень удобно, если у вас значений довольно много. Если бы они лежали в слайсе, вам бы пришлось все перебирать, а так вы сразу идете в нужное место.
При помощи ключевого слова map, потом в квадратных скобках идет тип ключа и тип данных. При этом вы можете, конечно же, сделать, например, мап мапа мапов. Что может выступать в качестве ключа? В качестве ключа может выступать любая сравниваемая структура данных. В примере мы объявили мапу и сразу же её инициализировали. Чтобы инициализировать, нужно указать ключ, через двоеточие — значение. Запятая нужна для того, чтобы сказать компилятору, что там что- то идет дальше. При этом мапу можно точно так же создать, выделив сразу нужно место под нужное количество элементов, чтобы не приходилось расширять ее в рантайме.
// сразу с нужной ёмкостью
profile := make(map[string]string, 10)
// количество элементов
mapLength := len(user)
fmt.Printf("%d %+v\n", mapLength, profile)
При инициализации пустой мапы, по аналогии со срезом, ее значение равно nil. Так происходит потому что мапы в Go также являются по своей сути указателями на базовые типы данных. Чтобы инициализировать пустую мапу, воспользуемся встроенной функцией make() . Кстати, совсем не обязательно указывать длину вторым аргументом, как в случае со срезами, хотя и есть такая возможность. В примере мы инициализировали пустую мапу и теперь можем записывать в нее значения.
Запись значений в мапу схожа с массивом, только вместо индекса мы указываем ключ.
Итерация по мапе аналогична со срезами и массивами.
Получение значения из мапы происходит по обращению в квадратных скобках к нужному ключу. Также, по аналогии со срезами и массивами, можно инициализировать мапу сразу с неким набором значений.
Обращение к несуществующему элементу мапы вполне корректно, и будет содержать в себе нулевое значение типа.
Для того, чтобы проверить, содержится ли данный ключ в нашей мапе, Go может возвращать 2 значения по обращению к элементу мапы по ключу: само значение и bool , который true если такой элемент есть и наоборот.
Нельзя дублировать ключи в мапе, это приведет к ошибке компиляции.
Однако значение по ключам можно свободно перезаписывать.
Функция delete()
Используя встроенную функцию delete() , можно удалять ключи из мапы, указав в качестве аргумента саму мапу и ключ. Как мы уже говорили, мапы, как и срезы, являються указателями на область в памяти. Поэтому скопировав мапу в новую переменную, и удалив из нее элементы, это отобразиться также и на новой мапе.
Из этого так же выплывает и то, что мапы, как и срезы, при передаче в качестве аргумента функции передаются по ссылке. Это значит что любые изменения внутри тела функции также изменят саму мапу.
Типы ключей
Как упомянуто ранее, ключи карты могут быть любого типа, который сопоставим (comparable). Спецификация языка определяет это точно, но короче говоря, сопоставимые типы - это логические, числовые, строковые, указательные, каналы и интерфейсные типы, а также структуры или массивы, которые содержат только эти типы. В списке отсутствуют срезы, карты и функции; эти типы нельзя сравнивать с помощью == и нельзя использовать в качестве ключей карты.
Очевидно, что строки, целые числа и другие базовые типы должны быть доступны в качестве ключей карты, но, возможно, неожиданными являются ключи структуры. Struct может использоваться для ключевых данных по нескольким измерениям. Например, эту карту карт можно использовать для подсчета посещений веб-страниц по странам:
Это карта строк к (карте строк к int). Каждый ключ внешней карты - это путь к веб-странице со своей собственной внутренней картой. Каждый внутренний ключ карты представляет собой двухбуквенный код страны. Это выражение возвращает количество раз, когда австралиец загружал страницу документации:
К сожалению, этот подход становится громоздким при добавлении данных, так как для любого данного внешнего ключа вы должны проверить, существует ли внутренняя карта, и создать ее, если необходимо:
С другой стороны, дизайн, в котором используется одна карта с struct ключом, устраняет всю эту сложность:
Когда вьетнамец посещает домашнюю страницу, увеличивая (и, возможно, создавая) соответствующий счетчик за одну строку:
И точно так же просто увидеть, сколько швейцарцев прочитали спецификацию:
Конкурентность
Карты не безопасны для конкурентного использования: не определено, что происходит, когда вы читаете и пишете в них одновременно. Если вам нужно читать и записывать на карту из конкурентно выполняемых go-процедур (goroutines), доступ должен быть обеспечен каким-то механизмом синхронизации. Один из распространенных способов защиты карт - это sync.RWMutex.
Этот оператор объявляет переменную-счетчик, которая является анонимной структурой, содержащей карту и встроенный sync.RWMutex.
Для чтения со счетчика возьмите блокировку чтения (read lock):
Для записи в счетчик возьмите блокировку записи (write lock):
Порядок итерации
При итерации по карте с помощью range цикла порядок итераций не указывается и не гарантируется, что он будет одинаковым от одной итерации к следующей. Если вам требуется стабильный порядок итераций, вы должны поддерживать отдельную структуру данных, которая определяет этот порядок. В этом примере используется отдельный отсортированный срез ключей для печати map[int]string в порядке расположения ключей:
Если запрошенный ключ отсутствует, карты Go возвращают нулевое значение для типа значений карты. Поэтому вам нужен альтернативный способ отличить сохраненный ноль от отсутствующего ключа.
Поищем в карте заведомо несуществующее значение и посмотрим на результат:
Вывод должен выглядеть так:
Хотя ключ sammy
отсутствует в карте, Go возвращает значение 0
. Это связано с тем, что используется тип данных значения int
, и поскольку в Go задано нулевое значение всех переменных, возвращается нулевое значение 0
.
Во многих случаях такое поведение нежелательно и может вызвать ошибку в программе. При поиске значения на карте Go может вывести второе опциональное значение. Это второе значение является булевым значением
true
в случае обнаружения ключа или false
в случае отсутствия ключа. В Go это называется идиомой ok
. Хотя вы можете присвоить любое имя переменной, получающей второй аргумент, в Go всегда следует использовать имя ok
:
Если ключ sammy
существует в карте counts
, ok
будет иметь значение true.
В противном случае ok
будет иметь значение false.
Вы можете использовать переменную ok
для определения действий в программе:
Результат будет выглядеть следующим образом:
В Go вы можете комбинировать декларирование переменных и условную проверку с помощью блока if/else. Это позволяет использовать для такой проверки одно выражение:
При получении значения из карты в Go всегда полезно проверить его существование, чтобы избежать ошибок в программе.
Добавление или изменение элемента карты m:
Получить элемент:
Удалить элемент:
Проверить, что ключ присутствует с помощью присвоения двух значений:
Если key присутствует в m, ok будет true. Если нет, ok - false.
Если key не присутствует в карте, то elem будет нулевым значением типа элементов карты.
Примечание: если elem или ok не были объявлены, то вы можете использовать краткую форму:
Пример изменения карты:
Вывод:
Теперь рассмотрим такой тип составных данных, как структуры. По мере роста программы бывает неудобно моделировать сущности набором скаляр, и возникает желание как-то объединить их, чтобы они представляли собой единую сущность. Структуры — это как раз такое объединение.
Самый базовый синтаксис объявление структур имеет следующий синтаксис
varName := struct{}{}
Как вы можете заметить, после ключевого слова struct идет 2 пары фигурных скобок. В первой паре необходимо описать саму структуру, ее поля. Во второй паре скобок необходимо уже записать в эти поля значения. Давайте создадим свою первую структуру, которая описывает сущность сотрудника.
В теле структуры мы объявили имена полей (параметров сущности) и их типы. Вторые скобки мы оставили пустыми. Как вы можете увидеть в консоле, у переменной employee теперь есть поля, но все они заполнены нулевыми значениями. Теперь же мы присвоили значения полям во вторых скобках, и наша переменная employee при выводе в консоль отображает это.
Такое объявление структуры как в примере выше на практике используется достаточно редко. Дело в том, что зачастую мы хотим один раз описать структуру, а дальше создавать в нашей программе неограниченное количество ее экземпляров.
Структуры, как и все остальные типы в Go, объявляются через ключевое слово type, потом идёт имя этого типа, ключевое слово struct, и в фигурных скобках объявляются поля этой структуры.
Полями структуры может быть абсолютно любой тип, который есть в Go, например string или даже функция, или другая структура.
Как инициализируются структуры? Для того чтобы инициализировать структуры, можно воспользоваться либо полным форматом объявлений — в фигурных скобках указать нужные поля, при этом какие-то поля можно пропустить, и тогда они примут значения по умолчанию.
Либо же мы можем воспользоваться краткой формой объявления, тогда нам не нужно указывать имена всех полей структуры, но при этом нам нужно указать значения абсолютно для всех полей структуры.
Обращение к полям структуры происходит через точку. fmt.Println(acc.Name)
Иногда бывает так, что мы хотим больше композиции. В Go нет ООП в классическом понимании, однако вся работа с объектами в Go построена на композиции. Например, мы можем встроить одну структуру в другую. Делается это так.
В чем отличие Owner от Person? Дело в том, что Owner — это какое-то свойство структуры, а встроенный, заэмбедженный Person — это уже не свойство структуры, а часть самой структуры. И все поля Person являются частью структуры Account. Например, вы можете обращаться к этим полям непосредственно.
fmt.Println(acc.Adress)
То есть Address является частью поля структуры Person, но, поскольку мы встроили Person в Account, то мы можем обращаться к полям Person напрямую. Обратим внимание, что при объявлении Person’а встроенной структуры, всё равно нужно объявлять её через префикс.
Если, как в данном примере, в структуре есть поля с одинаковыми именами у структуры, в которую встраиваем, и у встраиваемой структуры, то никакого конфликта не будет. Оба поля сохранятся. При этом при обращении к этому полю приоритет будет к наиболее верхнему полю структуры. То есть при выполнении fmt.Println(acc.Name) будет выведено поле Account, в котором лежит rvasily, а не поле Person. Если же мы хотим всё-таки обратиться к полю встроенной структуры Person, мы должны явно это указать.
fmt.Println(acc.Person.Name)
Теперь выведется Василий.
Методы структур
Теперь рассмотрим методы. Метод — это какая-то функция, которая может быть привязана к определенному типу данных. Для начала рассмотрим методы, которые привязываются к структурам. Определим тип Person.
Метод отличается от обычной функции только тем, что перед определением имени этой функции добавляется имя типа, для которого определен этот метод, с указанием роли получателя. Роль получателя — это то, в каком виде этот метод получит тип, к которому привязан. Это может быть либо передача по значению, то есть вам в метод передается копия этого типа, либо по адресу. В первом случае в функции UpdateName любые изменения, вызванные в этом методе, оригинальную структуру не затронут. На самом деле функция UpdateName смысла не имеет. Если же мы в качестве роли указываем адрес на тип, то тогда все изменения, которые были внесены в структуру этим методом сохранятся. Попробуем выполнить.
Поле Name действительно обновляется. Но обратим внимание, что pers у нас не ссылка на структуру, а сама структура. В то время как «получателем» указан адрес структуры. Дело в том, что Go определяет, в каком виде мы хотим получить получателя, либо по значению, либо по адресу, и автоматически выполняет необходимые преобразования. Точно так же можно было вызвать функцию с помощью строчки
Возникает закономерный вопрос: что происходит с методами, если встраивать одну структуру в другую?
Правильно, метод наследуется. То есть структура account в примере может иметь доступ ко всем методам структур, которые в нее встроены. В данном случае она может иметь доступ к методу SetName. Давайте посмотрим, как это работает. Создадим Account, создадим там Person и вызовем функцию SetName
после вызова функции изменится значение в Person. Как вы помните, сохраняются оба поля name, который был и во встроенной структуре, и в структуре верхнего уровня. А что будет если объявить функцию для Аccount?
Теперь при вызове acc.SetName("romanov.vasily") изменится уже поле Name аккаунта, потому что его метод имеет больший приоритет, чем метод встроенной структуры. Если же теперь требуется вызвать метод непосредственно у встроенной структуры, то обратиться к нему можно, используя полный селектор. acc.Person.SetName("Test")
Используя такой подход, можно создавать очень мощные структуры данных. При этом методы могут быть не только у структур. Например, можно объявить какой-то тип
Конструкторы
Функция newEmployee принимает в качестве аргументов параметры структуры и создает новый экземпляр. Такие функции называются конструкторами или инициализаторами и зачастую записываются как newНазваниеСтруктуры() и возвращают новый объект самой структуры. Конструкторы пришли к нам из мира ООП, и по своей сути, не являются обязательной частью программы на Go. Однако, такой подход можно назвать хорошей практикой, и вы точно столкнетесь с такой конструкций в большинстве проектов. К отдельным полям объекта можно обращаться по имени поля после точки, как на примере выше.
Интерфейс — это абстрактный тип, который описывает поведение, но не реализовывает его. Интерфейсы описывают абстракцию (обобщают) поведение других типов. С помощью обобщения интерфейсы позволяют писать более гибкие и адаптируемые функции, не привязанные к деталям одной конкретной реализации. И в правду, все что делает интерфейс — это описывает поведение какой-либо сущности. Он не реализовывает, а лишь определяет набор методов, какие сущность данного типа должна реализовать.
Структуры и типы могут реализовывать (или имплементировать) некий интерфейс. Тип соответствует (удовлетворяет) интерфейсу, если он обладает всеми методами, которые требует интерфейс.
Через интерфейсы в Go реализован полиморфизм, то есть возможность функции принимать аргументы различных типов. В отличие от таких языков, как C++, Java и PHP, где типизация явная, в Go реализована утиная типизация. Что это значит? В C++, когда вы создаете класс, вы явно указываете, что он реализует такой интерфейс. «Вот мои документы, вот я наследовался от этого интерфейса, я его реализую.Я — утка.Я точно знаю, что я — утка». В Go, когда вы создаете структуру с методами, она не знает, какому интерфейсу она соответствует, то есть типизация неявная. Там применяется другой подход: Если что-то крякает, как утка, плавает, как утка и летает, как утка, то это утка. То есть за соблюдение контракта этого интерфейса отвечает не сама структура, а метод, в который вы ее передаете. То есть вы передаете в метод, который принимает определенный интерфейс. А уже интерфейс описывает, что всем, кто хочет ему удовлетворять необходимо иметь определенные методы.
В примере показано, как объявить тип интерфейса. Сначала нужно указать служебное слово type, за- тем имя интерфейса и ключевое слово interface, чтобы указать, что этот тип является интерфейсом. В примере объявлен интерфейс Payer, «плательщик», для того чтобы соответствовать этому интерфейсу, нужно иметь метод pay, «заплатить», который принимает int и возвращает ошибку. Реализуем структуру «кошелек», у которой есть какое-то поле cash, то есть количество денег в этом кошельке, и есть метод «заплатить».
Обратите внимание: в реализации кошелька нигде нет упоминания того, что он каким-либо образом реализует интерфейс «плательщик». И теперь напишем функцию Buy, которая принимает в себя интерфейс «плательщик». Она не знает уже, какую именно структуру в неё передадут, но ей важно, чтобы то, что придет в эту функцию, обладало методом Pay.
то получим вывод «Спасибо за покупку через main.Wallet». При помощи %T в форматируемом выводе можно получить тип переданного аргумента. Это был простой пример, так как в нем была всего одна структура, реализующая интерфейс. Давайте теперь рассмотрим несколько более сложный пример, когда у есть несколько структур, которые реализуют интерфейс.
Реализована структура кошелек, в нем есть какое-то количество денег и реализован метод Pay. Реализо- вана структура карточка, в которой хранится баланс, дата, до которой она валидна, CVV, имя карточки, имя владельца, и она тоже реализует метод Pay. И есть ApplePay. В нем хранится количество денег на аккаунте и AppleID. И он тоже реализует метод Pay. Так же реализова интерфейс «плательщик», кото- рый требует, чтобы у типа, подаваемого на вход был метод Pay. И функция «купить». Попробуем этим воспользоваться.
В main создаем кошелек, и через него что-то покупаем. Теперь создаем не сразу какой-то объект, который дальше будем передавать, а переменную типа «плательщик». И теперь в эту переменную можно присва- ивать любые структуры, которые реализуют этот интерфейс. То есть проверка на интерфейс может быть не только при передаче в функцию, но и при присвоении в переменную. Соответственно при создании структуры можно указывать, что какое-то поле должно иметь определенный интерфейс, соответствовать определенному интерфейсу. И, наконец, присваиваем в эту же переменную, которая реализует интерфейс «плательщик», другую реализацию этого плательщика - ApplePay.
Теперь, если это запустить будет выведено:
Спасибо за покупку через main.Wallet
Спасибо за покупку через main.Card
Ошибка при оплате *main.ApplePay: Не хватает денег на аккаунте
Иногда бывает нужно не просто вызывать какие-то методы интерфейса, но и проверять, какая именно структура, удовлетворяющая интерфейсу поступила на вход. Для этих целей у нас есть специальная конструкция type switch. Реализуется она через оператор switch, p и запрос типа в скобочках. Перепишем функцию Buy с использованием type switch.
Смотрите, если интерфейс представлен типом «кошелек», выведем «Оплата наличными». Если интерфейс представлен типом Card, то получим доступ к данным этой карточки. Когда в функцию передается интерфейс, нельзя просто так обратиться к полям структуры, которая лежит под этим интерфейсом. Если так сделать, будет вызвана паника. Например, Cardholder undefined (type Payer has no field or method Cardholder). если мы пытаемся получить доступ к имени владельца. Если преобразование прошло успешно, в plasticCard действительно будет лежать тип Card, и теперь можно обращаться к его полям, это уже не интерфейс. Далее мы рассмотрим пустой интерфейс, который позволяет передать в себя вообще все что угодно.
После создание пустой переменной типа нашего интерфейса, ее значение равно nil и ее тип также равен nil . Сначала это может показаться вам немного сложным для понимания. Концептуально значение интерфейсного типа, или просто значение интерфейса, имеет два компонента — конкретный тип и значение этого типа. Они называются динамическим типом и динамическим значением интерфейса. Почему динамическим? В Go мы можем присваивать в переменную интерфейса другие типы, которые соответствуют данному интерфейсу. Как только значение интерфейса становиться не nil , его динамический тип становится типом нового значения. Именно поэтому мы без проблемы присвоили в переменную s типа storage объект типа memoryStorage — этот тип соответствует интерфейсу, потому что обладает всеми необходимыми методами. И поэтому после инициализации интерфейса, его значение и тип равны nil , а после присвоения нашей структуры, значение не равно nil, а тип равен memoryStorage .
Как мы уже говорили в самом начале, интерфейсы позволяют писать более гибкие и адаптируемые функции, не привязанные к деталям одной конкретной реализации. Свобода замены одного типа другим, который соответствует тому же интерфейсу, называется взаимозаменяемостью (substitutability) и является отличительной особенностью объектно-ориентированного программирования.
Пустые интерфейсы
Как мы уже говорили, интерфейс также может быть nil . Это интерфейс, который вообще не описывает никакого поведения. Тогда зачем нам это? Суть в том, что любой тип в программе на Go по умолчанию удовлетворяет пустой интерфейс. Возьмем для примера уже знакомую нам функцию fmt.Println() .
Если посмотреть на ее реализацию, то мы можем увидеть что в качестве аргументов она принимает неограниченное количество пустых интерфейсов — именно по этому мы можем в нее передавать любые типы: числа, интерфейсы, мапы, срезы, структуры и тд. Работая с пустыми интерфейсами, мы имеем возможность проверять интерфейс на его динамический тип. Давайте разберем следующий пример.
Наша функция принимает в качестве аргумента пустой интерфейс. В теле функции мы делаем серию проверок, проверяя это значение на конкретный тип, и выполняя в зависимости от этого разные действия. Для проверки типа используется следующая конструкция: value, ok := variable.(type) При проверке типа мы получаем 2 переменных: bool, который равен true если значение удовлетворяет данному типу и само значение, приведенное к нужному типу. В таких случаях, при проверке большого количества возможных значений одной переменной, чаще всего используется еще один условный оператор switch case . Давайте перепишем пример выше, используя эту конструкцию.
В функцию теперь можно передать всё, что угодно, и она должна проверить не на этапе компиляции соответствие интерфейсу, а уже в runtime’е.
Композиция интерфейсов
Теперь обсудим встраивание интерфейсов. С интерфейсами можно поступать подобно структурам, когда вы можете вложить одну структуру в другую и иметь доступ к её полям. Можете встраивать один интерфейс в другой, тем самым образуя более сложные интерфейсы. Давайте рассмотрим пример.
type Payer interface {
Pay(int) error
}
type Ringer interface {
Ring(string) error
}
type NFCPhone interface {
Payer
Ringer
}
Объявляем интерфейс Плательщик, который требует метод Pay. И интерфейс Звонилка, который требует метод позвонить. Теперь можно объявить интерфейс NFCPhone, то есть телефон, который реализует метод бесконтактной оплаты через NFC технологию. Этот интерфейс образован композицией двух других интерфейсов: Плательщик и Звонилка. То есть получается, что на самом деле мой интерфейс выглядит вот так.
Но каждый раз полностью объявлять совсем новые типы не очень удобно, поэтому композиция интер- фейсов позволяет облегчить жизнь. Не обязательно использовать только интерфейсы, один интерфейс можно встроить, а другой, например, объявить. Это также очень мощный способ композиции интерфей- сов, и много в стандартной библиотеке реализовано через него.
Назначение
Внутреннее устройство
Буферизация
Закрытие
Что будет, если писать в закрытый канал?
Что будет, если читать из закрытого канала?
Как закрывать канал со стороны читателя?
Зачем может потребоваться схема “много писателей - один читатель”?