Массивы, срезы, отображения
Перечислимые типы
Массивы
Пожалуй, массивы в языке Go не имеют ярких особенностей, что отличали бы в этом плане Go от прочих языков в ту или иную сторону. Итак, вспомним, что такое массив. В классическом понимании, массив есть индексированная последовательность однотипных элементов фиксированного размера. Конечно, есть языки, наподобие PHP или JavaScript, где массив - это сущность, которая позволяет с собою более вольное обращение, к языку Go это никак не относится. Объявляется массив вот так:
Мы объявили массив размерностью 5, присвоили каждому элементу значения, и, в завершение, вывели значения каждого элемента в цикле for. Как видите, ничего необычного. Как и в языке C, размерность массива задаётся при помощи константы. Нумерация элементов начинается с нуля. Разумеется, вместо константы можно использовать литерал и отказаться от использования отдельной переменной для хранения размерности массива, если это вам необходимо.
Встроенная функция len возвращает размерность массива. В связи с этим, она часто используется в циклах при переборе элементов. Выход за пределы массива вызывает аварийную ситуацию.
Как и всякая другая переменная, при объявлении внутри функции, массив может быть объявлен кратко, при помощи оператора :=
Во множестве случаев, длина массива может быть значительной. Для таких случаев предусмотрена возможность переносов строк при инициализации. Необходимо лишь запомнить, что в таком случае после замыкающего элемента также ставится запятая. Это будет единственной особенностью.
По массиву (и по срезу тоже) возможна своеобразная "навигация" путём указания значений в квадратных скобках. Например вот так:
Как видите, всё просто. Указываем в квадратных скобках желаемый начальный и конечный индекс (включительно). Если начальный индекс - 0, его можно не указывать, вот так: [:3]. Точно так же поступают и с последним индексом: [1:].
Массивы одного типа могут присваиваться и сравниваться. Под типом в данном случае понимается совокупность: размерность + тип элемента.
Для упрощения дела, я вывожу здесь значения массивов при помощи функции Println пакета fmt в "сыром" виде, чтобы не загромождать код циклами.
Этим, пожалуй, исчерпывается необходимая информация о массивах.
Срезы
В связи с фиксированным размером, массивы на самом деле используются в языке Go сравнительно нечасто. Скорее используются срезы, которые зачастую трактуют как динамический массив, что допустимо, но фактически не совсем верно. В английском оригинале срезы именуются slices, отчего их часто именуют просто "слайсами". В русскоязычной же технической литературе принят перевод "срез", поэтому мы будем придерживаться именно такой терминологии.
Объявить срез просто:
Значение nil является нулевым указателем. Оно аналогично понятию NULL в С-подобных языках. Это - зарезервированное слово в языке Go. Как вы уже поняли, приведенное выше объявление создаёт "нулевой" срез. Это, однако, не мешает работать с таким срезом, что мы покажем далее. А сейчас инициализируем срез.
Существует встроенная функция make, применяемая для инициализации срезов и отображений. Покажем, как она работает:
Не трудно заметить, в данном случае мы создали снова-таки пустой срез, но в данном случае он не равен значению nil, а получил определённую размерность. К тому же каждый из элементов среза был проинициализирован значением типа по умолчанию. Поскольку в нашем случае тип - integer, то соответственно, все элементы были проинициализированы значением 0.
Важно! В отличие от массивов, срез имеет свойство сравнимости лишь со значением nil, но не другим срезом.
Для разрешения проблемы сравнения двух срезов, я написал такую функцию:
Рекомендую вам также ею воспользоваться. Начните создавать собственную библиотеку функций. Напомню, в разделе о пакетах мы создали корневой каталог fqdn.org для хранения библиотек нашего кода. В нём мы создали папку common-utils для хранения файлов пакета utils. В пакете находился файл utils.go непосредственно с кодом. Полагаю, самое время добавить в этот файл новую функцию.
Замечу, что данная функция обрабатывает срезы одного типа - []int. Для обработки срезов иного типа необходимо заменить тип параметров функции на соответствующий - []string, []float64, []byte и так далее. Для создания же универсальной функции, нам потребуются более сложные инструменты, и, следовательно, больше знаний. Нам повезло. Так много нового впереди!
Append и copy - встроенные функции
Массивы неповоротливы, как испанские галеоны. Всё что можно сделать с массивом - присвоить один другому при условии однотипности, да сравнить. Срезы же гораздо гибче! Специально применительно к срезам заведены встроенные функции append и copy, призванные причинять всяческую пользу. Разберёмся же в них!
append
Эта встроенная функция позволяет добавить элемент в конец данного среза. Вот так:
Но что если нам понадобится объединить подобным образом два среза? Нет проблем! С помощью небольшой синтаксической хитрости вы можете сделать это.
Как это стало возможным? Резонный вопрос. Дело в том, что append - это так называемая вариативная функция. Знак многоточия означает, что количество параметров данного типа не определено. Таким образом, передаваемый в качестве аргумента срез рассматривается функцией как последовательность аргументов данного типа. Но не будем слишком отвлекаться и забегать вперёд. Предлагаю о функциях побеседовать подробнее в соответствующей главе. А сейчас - просто запомните данный кейс.
Интересный момент: срез - аргумент можно задать без создания переменной,"на лету":
Непроинициализированный, "пустой" срез, значение которого nil, не помеха тому, чтобы в него успешно "аппендить":
copy
"Говорящее" название этой встроенной функции лишает нас необходимости размышлять о её назначении. Просто возьмём, и скопируем один срез в другой.
Как видите, второй аргумент копируется в первый. При несовпадении размерности срезов, поведение функции будет зависеть от того, который из срезов больше. Если в первом аргументе содержится больше элементов, будут скопированы все элементы второго среза, лишние же останутся нетронутыми:
В противоположной ситуации, то есть при меньшей размерности первого среза, избыточные элементы второго среза будут проигнорированы (отброшены):
Приём "работа на месте"
Вернитесь на пару-тройку глав назад, и вспомните наше недавнее знакомство с таким синтаксическим приёмом, как "присваивание кортежу". Этот приём неоценим в работе с типами данных, представляющими из себя индексированные последовательности элементов. Собственно, ценность заключается в факте возможности так называемой "работы на месте" с элементами, что позволяет писать оптимизированные программы, то есть экономить память. приведу пример, который рекомендую вам перенести в свою библиотеку полезных функций.
Эта функция делает реверс "на месте" элементов заданного среза. Соберите информацию по теме endianness, и вам сразу станет интересно приведенное выше. Впрочем, мы, скорее всего, ещё коснёмся данного вопроса. Скучно не будет!
Срезы: а что под капотом?
Практически, мы использовали срезы как динамические массивы. Это допустимо, но необходимо понимать, что это лишь удобное упрощение, редукция. Настало время разобраться, что же такое представляет собою срез в языке Go фактически. Суть дела заключается в том, что всякий срез привязан к подлежащему так называемому базовому массиву. Он содержит такие понятия, как указатель, длину и ёмкость. Указатель "глядит" на первый элемент массива, доступный через срез. Благодаря этому, изменение среза, приводит и к изменению базового массива, в том числе и при передаче среза в функцию в качестве аргумента. Длина берётся при помощи уже знакомой нам встроенной функции len. В свою очередь, функция cap даёт нам значение ёмкости, которая есть количество элементов между началом среза и концом базового массива. Таким образом, когда мы создаём срез, "под капотом" на деле создаётся массив, с которого и берётся собственно "срез". С другой стороны, можно сначала явно создать массив, а затем "привязать" к нему срез, или несколько срезов, в том числе, возможно, перекрывающих друг друга. А ещё, при использовании функции make, можно третьим параметром указать емкость базового массива, отличную от создаваемого среза. Попытаемся проиллюстрировать сказанное.
Простое объявление среза. Что делает компилятор на самом деле?
Явное объявление базового массива с последующим взятием с него срезов:
Важная деталь! При взятии среза из элементов базового массива в диапазоне X...Y указывается значение Y+1. Как следует из примера выше, срез workDays содержит элементы с 0 по 4 включительно, однако записывается это как workDays := week[:5]! Будьте внимательны!
И, наконец, продемонстрируем третий параметр функции make:
По факту, создаётся базовый массив ёмкостью 10 элементов и с него берётся срез, являющийся указателем на нулевой элемент этого массива, и имеющий длину 5 элементов.
Сделаем выводы. Мы осознали, что срезы - это замечательный, легковесный инструмент для работы с последовательностями однотипных данных, который на практике используется гораздо чаще и, пожалуй, более охотно, чем массивы. Цена же этого - некоторая сложность фактической реализации, которую необходимо ясно себе представлять, чтобы грамотно ориентироваться в материале.
One more thing...
Для вывода на экран содержимого среза, правильнее всего использовать цикл for. При работе с большим количеством однотипных срезов, во избежание загромождения кода циклами и следуя концепции "The DRY Principle: Don't Repeat Yourself", рекомендую добавить в свою библиотеку такую функцию:
Разумеется, для обработки срезов различного типа, функция нуждается в корректировке параметра. Подобную же функцию можно использовать и для "распечатки" массивов. Для этого лишь нужно в квадратных скобках указать размерность массива-параметра.
Для особо любознательных и нетерпеливых читателей сообщу, что для работы с байтовыми массивами существует встроенный пакет bytes. Строки также можно трактовать как разновидность массивов, и для работы с ними существует пакет strings, во многом аналогичный брату-близнецу bytes. Конечно, это будет большим забеганием вперёд.
Отображения
Во всякой науке первейшая вещь - дефиниция. Отображения - как раз тот случай, когда следует об этом поговорить. В английском оригинале эта сущность определяется как maps. Поэтому некоторые не мудрствуя лукаво обзывают это просто "мапами", или "картами", на худой конец. В серьёзных же источниках в основном принят термин "отображения", который мы и будем употреблять, как наиболее академический. Что же по факту представляют собой maps? Отображения - это ассоциативные массивы по типу ключ - значение, где как ключ, так и значение могут быть любого типа (это может быть даже другое отображение). Очень близки к отображениям словари (dictionary) из C#. С математической точки зрения отображения - это истинные множества, так как это есть неупорядоченные совокупности уникальных данных.
Объявим отображение. Отметим, что в отличие от срезов, мы обязаны сразу проинициализировать наше отображение. Это делается при помощи встроенной функции make, либо в результате сокращённой инициализации. В противном случае отображение всё же будет объявлено (со значением nil), однако дальнейшая работа с таким отображением вызовет исключение:
Вот так правильно:
Для добавления элемента используется простое присваивание значения по ключу.
Отображения позволяется инициализировать сокращённо:
Для удаления элементов отображения используется встроенная функция delete.
Это немного необычно, но попытка вывести на экран или удалить при помощи функции delete несуществующий элемент, не вызовет никакой ошибки! И всё же, вероятно, прежде чем приступить к каким-либо действиям, вы можете захотеть проверить наличие того или иного элемента в данном отображении. Такая возможность существует. Доступ к элементу отображения по ключу может опционально возвращать не только один элемент - собственно значение, но и булев флаг, означающий принадлежность данного ключа множеству, составляющему это отображение.
В завершение, приведу пример, когда значением отображения является другое отображение:
Пожалуй, самое последнее... Отображения отлично сериализуются! Благодаря своей гибкости и полиморфности, они отлично зарекомендовали себя при работе с JSON в качестве полей структур. Но об этом - в своё время.
Раунд!
Last updated
Was this helpful?