Tutorial

Массивы и срезы в Go

GoDevelopment

Введение

В Go массивы и *срезы *представляют собой структуры данных, состоящие из упорядоченных последовательностей элементов. Эти наборы данных очень удобно использовать, когда вам требуется работать с большим количеством связанных значений. Они позволяют хранить вместе связанные данные, концентрировать код и одновременно применять одни и те же методы и операции к нескольким значениям.

Хотя и массивы, и срезы в Go представляют собой упорядоченные последовательности элементов, между ними имеются существенные отличия. Массив в Go представляет собой структуру данных, состоящую из упорядоченной последовательности элементов, емкость которой определяется в момент создания. После определения размера массива его нельзя изменить. Срез — это версия массива с переменной длиной, дающая разработчикам дополнительную гибкость использования этих структур данных. Срезы — это то, что обычно называют массивами в других языках.

С учетом этих отличий массивы и срезы предпочтительнее использовать в определенных ситуациях. Если вы только начинаете работать с Go, вам может быть сложно определить, что использовать в каком-либо конкретном случае. Благодаря универсальному характеру срезов, они будут полезнее в большинстве случаев, однако в некоторых ситуациях именно массивы могут помочь оптимизировать производительность программы.

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

Массивы

Массивы представляют собой структурированные наборы данных с заданным количеством элементов. Поскольку массивы имеют фиксированный размер, память для структуры данных нужно выделить только один раз, в то время как для структур данных переменной длины требуется динамическое выделение памяти в большем или меньшем объеме. Хотя из-за фиксированной длины массивов они не отличаются гибкостью в использовании, одноразовое выделение памяти позволяет повысить скорость и производительность вашей программы. В связи с этим, разработчики обычно используют массивы при оптимизации программ, в том числе, когда для структур данных не требуется переменное количество элементов.

Определение массива

Массивы определяются посредством декларирования размера массива в квадратных скобках [ ], после которых указывается тип данных элементов. Все элементы массива в Go должны относиться к одному и тому же типу данных. После типа данных вы можете декларировать отдельные значения элементов массива в фигурных скобках { }.

Ниже показана общая схема декларирования массива:

[capacity]data_type{element_values}

Примечание: важно помнить, что в каждом случае декларирования нового массива создается отдельный тип. Поэтому, хотя [2]int и [3]int содержат целочисленные элементы, из-за разницы длины типы данных этих массивов несовместимы друг с другом.

Если вы не декларируете значения элементов массива, по умолчанию используются нулевые значения, т. е. по умолчанию элементы массива будут пустыми. Это означает, что целочисленные элементы будут иметь значение 0, а строки будут пустыми.

Например, следующий массив numbers имеет три целочисленных элемента, у которых еще нет значения:

var numbers [3]int

Если вы выведете массив numbers, результат будет выглядеть следующим образом:

Output
[0 0 0]

Если вы хотите назначить значения элементов при создании массива, эти значения следует поместить в фигурные скобки. Массив строк с заданными значениями будет выглядеть следующим образом:

[4]string{"blue coral", "staghorn coral", "pillar coral", "elkhorn coral"}

Вы можете сохранить массив в переменной и вывести его:

coral := [4]string{"blue coral", "staghorn coral", "pillar coral", "elkhorn coral"}
fmt.Println(coral)

Запуск программы с вышеуказанными строчками даст следующий результат:

Output
[blue coral staghorn coral pillar coral elkhorn coral]

Обратите внимание, что при печати элементы массива не разделяются, и поэтому сложно сказать, где заканчивается один элемент и начинается другой. Поэтому иногда бывает полезно использовать функцию fmt.Printf вместо простой печати, поскольку данная функция форматирует строки перед их выводом на экран. Используйте с этой командой оператор %q, чтобы функция ставила кавычки вокруг значений:

fmt.Printf("%q\n", coral)

Результат будет выглядеть следующим образом:

Output
["blue coral" "staghorn coral" "pillar coral" "elkhorn coral"]

Теперь все элементы заключены в кавычки. Оператор \n предписывает добавить в конце символ возврата строки.

Теперь вы понимаете основные принципы декларирования массивов и их содержание, и мы можем перейти к изложению того, как задавать элементы в массиве по номеру индекса.

Индексация массивов (и срезов)

Каждый элемент массива (и среза) можно вызвать отдельно, используя индексацию. Каждый элемент соответствует номеру индекса, который представляет собой значение int, начиная с номера индекса 0 с увеличением в восходящем порядке.

В следующих примерах мы будем использовать массив, однако данные правила верны и для срезов, поскольку индексация массивов и срезов выполняется одинаково.

Для массива coral индекс будет выглядеть следующим образом:

“blue coral” “staghorn coral” “pillar coral” “elkhorn coral”
0 1 2 3

Первый элемент, строка "blue coral", начинается с индекса 0, а заканчивается срез индексом 3 с элементом "elkhorn coral".

Поскольку каждый элемент среза или массива имеет соответствующий ему номер индекса, мы можем получать доступ к этим элементам и выполнять с ними манипуляции точно так же, как и с другими последовательными типами данных.

Теперь мы можем вызвать дискретный элемент среза по его номеру индекса:

fmt.Println(coral[1])
Output
staghorn coral

Номера индекса для этого среза входят в диапазон 0-3, как показано в предыдущей таблице. Поэтому для вызова любого отдельного элемента мы будем ссылаться на номера индекса, как показано здесь:

coral[0] = "blue coral"
coral[1] = "staghorn coral"
coral[2] = "pillar coral"
coral[3] = "elkhorn coral"

Если мы вызовем массив coral с любым номером индекса больше 3, результат будет за пределами диапазона и запрос будет недействителен:

fmt.Println(coral[18])
Output
panic: runtime error: index out of range

При индексации массива или среза всегда следует использовать положительные числа. В отличие от некоторых языков, поддерживающих обратную индексацию с использованием отрицательных чисел, в Go такая индексация приводит к ошибке:

fmt.Println(coral[-1])
Output
invalid array index -1 (index must be non-negative)

Мы можем объединять элементы строк массива или среза с другими строками, используя оператор +:

fmt.Println("Sammy loves " + coral[0])
Output
Sammy loves blue coral

Например, мы можем объединить элемент строки с номером индекса 0 со строкой "Sammy loves ".

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

Изменение элементов

Мы можем использовать индексацию для изменения элементов массива или среза, задав для индексированного элемента другое значение. Это дает нам дополнительные возможности контроля данных в срезах и массивах и позволяет программно изменять отдельные элементы.

Если мы хотим изменить значение строки элемента с индексом 1 в массиве coral с "staghorn coral" на "foliose coral", мы можем сделать это так:

coral[1] = "foliose coral"

Теперь, когда мы будем распечатывать массив coral, он будет выглядеть по другому:

fmt.Printf("%q\n", coral)
Output
["blue coral" "foliose coral" "pillar coral" "elkhorn coral"]

Теперь вы знаете, как выполнять манипуляции с отдельными элементами массива или среза, и мы рассмотрим несколько функций, дающих дополнительную гибкость при работе с типами данных в коллекциях.

Подсчет элементов с помощью len()

В Go имеется встроенная функция len(), помогающая работать с массивами и срезами. Как и в случае со строками, вы можете рассчитать длину массива или среза, используя команду len() с указанием массива или среза в качестве параметра.

Например, чтобы определить количество элементов внутри массива coral, мы используем следующую команду:

len(coral)

Если вы распечатаете длину массива coral, результат будет выглядеть следующим образом:

Output
4

Это дает длину массива 4 в типе данных int, что соответствует действительности, поскольку массив coral содержит четыре элемента:

coral := [4]string{"blue coral", "foliose coral", "pillar coral", "elkhorn coral"}

Если вы создадите массив целых чисел с большим количеством элементов, вы можете использовать функцию len() и в этом случае:

numbers := [13]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
fmt.Println(len(numbers))

Результат будет выглядеть следующим образом:

Output
13

Хотя примеры массивов содержат относительно немного элементов, функция len() особенно полезна при определении количества элементов внутри очень больших массивов.

Далее мы рассмотрим процедуру добавления элемента в тип данных коллекции и покажем, как это сделать, поскольку в связи с фиксированной длиной массивов добавление таких статических типов данных может повлечь за собой ошибку.

Добавление элементов с помощью append()

append() — это встроенный метод Go для добавления элементов в тип данных коллекции. Но данный метод не будет работать с помощью массива. Как уже отмечалось, основное отличие массивов от срезов заключается в том, что размер массива нельзя изменить. Это означает, что хотя вы можете изменять значения элементов в массиве, вы не можете сделать массив больше или меньше после его определения.

Рассмотрим наш массив coral:

coral := [4]string{"blue coral", "foliose coral", "pillar coral", "elkhorn coral"}

Допустим, вы хотите добавить в массив элемент "black coral". Если вы попробуете использовать функцию append() в массиве с помощью следующей команды:

coral = append(coral, "black coral")

В результате вы получите сообщение об ошибке:

Output
first argument to append must be slice; have [4]string

Для решения подобных проблем нам нужно больше узнать о типе данных среза, определении среза и процедуре конвертации массива в срез.

Срезы

Срез *— это тип данных Go, представляющий собой *мутируемую или изменяемую упорядоченную последовательность элементов. Поскольку размер срезов не постоянный, а переменный, его использование сопряжено с дополнительной гибкостью. При работе с наборами данных, которые в будущем могут увеличиваться или уменьшаться, использование среза обеспечит отсутствие ошибок при попытке изменения размера набора. В большинстве случаев возможность изменения стоит издержек перераспределения памяти, которое иногда требуется для срезов, в отличие от массивов. Если вам требуется сохранить большое количество элементов или провести итерацию большого количества элементов, и при этом вам нужна возможность быстрого изменения этих элементов, вам подойдет тип данных среза.

Определение среза

Срезы определяются посредством декларирования типа данных, перед которым идут пустые квадратные скобки ([]) и список элементов в фигурных скобках ({}). Вы видите, что в отличие от массивов, для которых требуется поставить в скобки значения int для декларирования определенной длины, в срезе скобки пустые, что означает переменную длину.

Создадим срез, содержащий элементы строкового типа данных:

seaCreatures := []string{"shark", "cuttlefish", "squid", "mantis shrimp", "anemone"}

При выводе среза мы видим содержащиеся в срезе элементы:

fmt.Printf("%q\n", seaCreatures)

Результат будет выглядеть следующим образом:

Output
["shark" "cuttlefish" "squid" "mantis shrimp" "anemone"]

Если вы хотите создать срез определенной длины без заполнения элементов коллекции, вы можете использовать встроенную функцию make():

oceans := make([]string, 3)

При печати этого среза вы получите следующий результат:

Output
["" "" ""]

Если вы хотите заранее выделить определенный объем памяти, вы можете использовать в команде make() третий аргумент:

oceans := make([]string, 3, 5)

При этом будет создан обнуленный срез с длиной 3 и заранее выделенной емкостью в 5 элементов.

Теперь вы знаете, как декларировать срез. Однако это не решает проблему с массивом coral, которая возникала у нас ранее. Чтобы использовать функцию append() с coral, нужно вначале научиться преобразовывать разделы массива в срезы.

Разделение массивов на срезы

Используя числовые индексы для определения начальных и конечных точек, вы можете вызывать подразделы значений внутри массива. Эта операция называется разделением массива на слайсы, и вы можете сделать это посредством создания диапазона числовых индексов, разделенных двоеточием, в форме:[first_index:second_index]. Важно отметить, что при разделении массива на срезы в результате получается срез, а не массив.

Допустим, вы хотите вывести средние элементы массива coral, не включая первый и последний элемент. Для этого вы можете создать срез, начинающийся с индекса 1 и заканчивающийся перед индексом 3:

fmt.Println(coral[1:3])

Запуск программы с этой строкой даст следующий результат:

Output
[foliose coral pillar coral]

При создании среза (например, [1:3]), первое число означает начало среза (включительно), а второе число — это сумма первого числа и общего количества элементов, которое вы хотите получить:

array[starting_index : (starting_index + length_of_slice)]

В этом случае вы вызываете второй элемент (или индекс 1) в качестве начальной точки, а всего вызываете два элемента. Результат будет выглядеть следующим образом:

array[1 : (1 + 2)]

Вот как это было получено:

coral[1:3]

Если вы хотите задать начало или конец массива в качестве начальной или конечной точки среза, вы можете пропустить одно из чисел в синтаксисе array[first_index:second_index]. Например, если вы хотите вывести первые три элемента массива coral, а именно "blue coral", "foliose coral" и "pillar coral", вы можете использовать следующий синтаксис:

fmt.Println(coral[:3])

В результате будет выведено следующее:

Output
[blue coral foliose coral pillar coral]

Команда распечатала начало массива, остановившись непосредственно перед индексом 3.

Чтобы включить все элементы до конца массива, нужно использовать обратный синтаксис:

fmt.Println(coral[1:])

Получившийся срез будет выглядеть следующим образом:

Output
[foliose coral pillar coral elkhorn coral]

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

Преобразование массива в срез

Если вы создали массив и считаете, что для него требуется переменная длина, вы можете преобразовать этот массив в срез. Чтобы преобразовать массив в срез, используйте процесс разделения на срезы, описанный в разделе Разделение массивов на срезы настоящего документа, но пропустите указание обоих числовых индексов, определяющих конечные точки:

coral[:]

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

coralSlice := coral[:]

Если вы выводите coralSlice, результат будет выглядеть следующим образом:

Output
[blue coral foliose coral pillar coral elkhorn coral]

Теперь попробуйте добавить элемент black coral как в разделе массива, используя функцию append() в новом конвертированном срезе:

coralSlice = append(coralSlice, "black coral")
fmt.Printf("%q\n", coralSlice)

В результате будет выведен срез с добавленным элементом:

Output
["blue coral" "foliose coral" "pillar coral" "elkhorn coral" "black coral"]

В одном выражении append() можно добавить несколько элементов:

coralSlice = append(coralSlice, "antipathes", "leptopsammia")
Output
["blue coral" "foliose coral" "pillar coral" "elkhorn coral" "black coral" "antipathes" "leptopsammia"]

Чтобы объединить два среза также можно использовать выражение append(), но при этом необходимо раскрыть аргумент второго элемента, используя синтаксис расширения ...:

moreCoral := []string{"massive coral", "soft coral"}
coralSlice = append(coralSlice, moreCoral...)
Output
["blue coral" "foliose coral" "pillar coral" "elkhorn coral" "black coral" "antipathes" "leptopsammia" "massive coral" "soft coral"]

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

Удаление элемента из среза

В отличие от других языков, в Go отсутствуют встроенные функции для удаления элементов из среза. Для удаления элементов из среза их нужно вырезать.

Чтобы удалить элемент, нужно выделить в срез элементы до него, затем элементы после него, а затем объединить два новых среза в один срез, не содержащий удаленного элемента.

Если i — индекс удаляемого элемента, формат этого процесса будет выглядеть следующим образом:

slice = append(slice[:i], slice[i+1:]...)

Удалим из среза coralSlice элемент "elkhorn coral". Этот элемент располагается на позиции индекса 3.

coralSlice := []string{"blue coral", "foliose coral", "pillar coral", "elkhorn coral", "black coral", "antipathes", "leptopsammia", "massive coral", "soft coral"}

coralSlice = append(coralSlice[:3], coralSlice[4:]...)

fmt.Printf("%q\n", coralSlice)
Output
["blue coral" "foliose coral" "pillar coral" "black coral" "antipathes" "leptopsammia" "massive coral" "soft coral"]

Теперь элемент на позиции индекса 3, строка "elkhorn coral", больше не находится в срезе coralSlice.

Такой же подход можно применить и для удаления диапазона элементов. Допустим, мы хотим удалить не только элемент "elkhorn coral", но и элементы "black coral" и "antipathes". Для этого мы можем использовать в выражении диапазон:

coralSlice := []string{"blue coral", "foliose coral", "pillar coral", "elkhorn coral", "black coral", "antipathes", "leptopsammia", "massive coral", "soft coral"}

coralSlice = append(coralSlice[:3], coralSlice[6:]...)

fmt.Printf("%q\n", coralSlice)

Этот код убирает из среза индексы 3, 4 и 5:

Output
["blue coral" "foliose coral" "pillar coral" "leptopsammia" "massive coral" "soft coral"]

Теперь вы знаете, как добавлять и удалять элементы среза, и мы перейдем к измерению объема данных, который может храниться в срезе в любой момент времени.

Измерение емкости среза с помощью cap()

Поскольку срезы имеют переменную длину, для определения размера этого типа данных метод len() подходит не очень хорошо. Вместо него лучше использовать функцию cap() для определения емкости слайса. Данная функция показывает, сколько элементов может содержать срез. Емкость определяется объемом памяти, который уже выделен для этого среза.

Примечание: поскольку длина и емкость массива всегда совпадают, функция cap() не работает с массивами.

Функция cap() обычно используется для создания среза с заданным числом элементов и заполнения этих элементов с помощью программных методов. Это позволяет предотвратить выделение лишнего объема памяти при использовании команды append() для добавления элементов сверх выделенной емкости.

Допустим, мы хотим составить список чисел от 0 до 3. Мы можем использовать для этого функцию append() в цикле или мы можем заранее выделить срез и использовать функцию cap() в цикле для заполнения значений.

Вначале рассмотрим использование append():

numbers := []int{}
for i := 0; i < 4; i++ {
    numbers = append(numbers, i)
}
fmt.Println(numbers)
Output
[0 1 2 3]

В этом примере мы создали срез, а затем создали цикл for с четырьмя итерациями. Каждая итерация добавляла текущее значение переменной цикла i к индексу среза numbers. Однако это могло повлечь выделение ненужного объема памяти, что могло бы замедлить реализацию программы. При добавлении к пустому срезу при каждом вызове функции append программа проверяет емкость среза. Если при добавлении элемента емкость среза превышает это значение, программа выделяет дополнительную память с учетом этого. При этом возникают дополнительные издержки программы, которые могут замедлить выполнение.

Теперь заполним срез без использования append() посредством выделения определенной длины / емкости:

numbers := make([]int, 4)
for i := 0; i < cap(numbers); i++ {
    numbers[i] = i
}

fmt.Println(numbers)

Output
[0 1 2 3]

В этом примере мы использовали make() для создания среза и предварительно выделили 4 элемента. Затем мы использовали функцию cap() в цикле для итерации по всем обнуленным элементам, заполняя каждый до достижения выделенной емкости. В каждом цикле мы поместили текущее значение переменной цикла i в индекс среза numbers.

Хотя с функциональной точки зрения использование append() и cap() эквивалентно, в примере с cap() не выделяется лишняя память, которая потребовалась бы при использовании функции append().

Построение многомерных срезов

Также вы можете определять срезы, содержащие в качестве элементов другие срезы, при этом каждый список в скобках содержится также в скобках родительского среза. Такие наборы срезов называются многомерными срезами. Их можно представить как описание многомерных координат. Например, набор из пяти срезов, каждый из которых содержит шесть элементов, можно представить как двухмерную сетку с длиной пять и высотой шесть.

Рассмотрим следующий многомерный срез:

seaNames := [][]string{{"shark", "octopus", "squid", "mantis shrimp"}, {"Sammy", "Jesse", "Drew", "Jamie"}}

Чтобы получить доступ к элементу этого среза, нам нужно использовать несколько индексов, каждый из которых соответствует одному измерению конструкции:

fmt.Println(seaNames[1][0])
fmt.Println(seaNames[0][0])

В приведенном выше коде мы вначале определяем элемент с индексом 0 среза с индексом 1, а затем указываем элемент с индексом 0 среза с индексом 0. Результат будет выглядеть так:

Output
Sammy shark

Далее идут значения индекса для остальных отдельных элементов:

seaNames[0][0] = "shark"
seaNames[0][1] = "octopus"
seaNames[0][2] = "squid"
seaNames[0][3] = "mantis shrimp"

seaNames[1][0] = "Sammy"
seaNames[1][1] = "Jesse"
seaNames[1][2] = "Drew"
seaNames[1][3] = "Jamie"

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

Заключение

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

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

Creative Commons License