Слайсы в Go: использование и особенности
Это перевод статьи Go Slices: usage and internals из официального блога The Go Blog.
Слайсы в Go предоставляют удобный и эффективный способ работы с последовательностями данных одного типа. Слайсы аналогичны массивами в других языках программирования, но у слайсов есть кое-какие необычные свойства. В этой статье мы взглянем подробно на них: что это, как они работают и как их используют.
О массивах
Слайс - это абстракция, построенная на основе массивов, поэтому, чтобы разобраться со слайсами, нужно сначала разобраться с массивами.
По определению тип массива состоит из длины и типа его элементов. Например, тип [4]int
представляет массив из четырёх целых чисел. Размер массива неизменяем; его длина - это часть его типа ([4]int
и [5]int
различные, несовместимые типы). Массивы могут быть проиндексированы, поэтому с помощью выражения s[n]
мы получаем доступ к n-ному элементу, начиная с нуля.
Массивы не нужно инициализировать явно; нулевой массив - это готовый к использованию массив, элементы которого являются нулями:
Представление [4]int в памяти - это просто четыре целых значения, расположенных последовательно:
Массивы в Go и есть значения. Переменная с именем массива обозначает весь массив; это не указатель на первый элемент (как это было бы в случае С). Это значит, что когда вы присваиваете значение или проходитесь по массиву, вы будете делать копию его содержимого (для избежания копирования, вы могли бы передавать указатель на массив, но тогда это будет указатель на него, а не сам массив). Один из способов представить массив - это будто массив является как бы структурой, но с нумерованными, а не именованными полями: составное значение фиксированного размера.
Литерал массива может быть задан так:
Или вы можете указать компилятору посчитать количество значений:
В обоих случаях у b
будет тип [2]string
.
Слайсы
Массивы имеют право на существование, но они немного негибкие, поэтому вы не увидите их так часто в коде на 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
. Оставим это в качестве упражнения для читателя.
Полезные ссылки:
- A Tour of Go: Appending to a slice
- Effective Go: Slices, Arrays
- Go language specifications: Slices