The Go Blog
Go Slices: usage and internals
Introduction
Тип среза в Go предоставляет удобный и эффективный способ работы с последовательностями данных определённого типа. Срезы аналогичны массивам в других языках программирования, но обладают некоторыми необычными свойствами. В этой статье будет рассмотрено, что такое срезы и как они используются.
Массивы
Тип среза является абстракцией, построенной поверх типа массива в Go, и поэтому, чтобы понять срезы, необходимо сначала понимать массивы.
Определение типа массива указывает длину и тип элемента.
Например, тип [4]int представляет массив из четырёх целых чисел.
Размер массива фиксирован; его длина является частью его типа ([4]int и [5]int являются различными,
несовместимыми типами).
Массивы можно индексировать обычным способом, так что выражение s[n] обращается
к n-ному элементу, начиная с нуля.
<code>var a [4]int a[0] = 1 i := a[0] // i == 1 </code>
Массивы не требуют явной инициализации; нулевое значение массива — это готовый к использованию массив, элементы которого также нулевые:
<code>// a[2] == 0, нулевое значение типа int </code>
Внутреннее представление [4]int — это просто четыре целочисленных значения, расположенных последовательно:
Массивы в Go являются значениями. Переменная массива обозначает весь массив; это не указатель на первый элемент массива (как это происходит в C). Это означает, что при присваивании или передаче значения массива будет сделана копия его содержимого. (Чтобы избежать копирования, можно передать указатель на массив, но тогда это будет указатель на массив, а не сам массив.) Одним из способов мышления о массивах является представление их как структуры с индексированными полями вместо именованных: фиксированный по размеру составной тип.
Литерал массива может быть указан следующим образом:
<code>b := [2]string{"Penn", "Teller"}
</code>
Или же можно позволить компилятору подсчитать количество элементов:
<code>b := [...]string{"Penn", "Teller"}
</code>
В обоих случаях тип b будет [2]string.
Срезы
Массивы имеют своё место, но они довольно не гибкие, поэтому вы редко видите их в коде на Go. Срезы же повсеместны. Они строятся поверх массивов, предоставляя при этом огромную мощь и удобство.
Спецификация типа среза — []T,
где T — это тип элементов среза.
В отличие от типа массива, тип среза не имеет заданной длины.
Литерал среза объявляется так же, как и литерал массива, за исключением того, что вы опускаете количество элементов:
<code>letters := []string{"a", "b", "c", "d"}
</code>
Срез может быть создан с помощью встроенной функции под названием make, которая имеет сигнатуру,
<code>func make([]T, len, cap) []T </code>
где T обозначает тип элемента создаваемого среза.
Функция make принимает тип, длину и необязательную ёмкость.
При вызове make выделяется массив и возвращается срез, ссылающийся на этот массив.
<code>var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}
</code>
Если аргумент ёмкости опущен, он по умолчанию равен указанной длине. Вот более краткая версия того же кода:
<code>s := make([]byte, 5) </code>
Длину и ёмкость среза можно проверить с помощью встроенных функций len и cap.
<code>len(s) == 5 cap(s) == 5 </code>
Следующие два раздела рассматривают связь между длиной и ёмкостью.
Нулевое значение среза — nil. Функции len и cap будут возвращать 0 для нулевого среза.
Срез также может быть создан путём «нарезки» существующего среза или массива.
Нарезка выполняется путём указания полуоткрытого диапазона с двумя индексами, разделёнными двоеточием.
Например, выражение b[1:4] создаёт срез, включающий элементы
1 через 3 массива b (индексы результирующего среза будут 0 через 2).
<code>b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, разделяет ту же память, что и b
</code>
Начальный и конечный индексы выражения среза являются необязательными; по умолчанию они равны нулю и длине среза соответственно:
<code>// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b
</code>
Это также синтаксис для создания среза из массива:
<code>x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // срез, ссылающийся на память массива x
</code>
Внутреннее устройство срезов
Срез — это дескриптор сегмента массива. Он состоит из указателя на массив, длины сегмента и его ёмкости (максимальной длины сегмента).
Наша переменная s, созданная ранее с помощью make([]byte, 5), выглядит следующим образом:
Длина — это количество элементов, на которые ссылается срез. Ёмкость — это количество элементов в базовом массиве (начиная с элемента, на который ссылается указатель среза). Разница между длиной и ёмкостью станет ясной при рассмотрении следующих примеров.
При нарезке s обратите внимание на изменения в структуре данных среза и их связь с базовым массивом:
<code>s = s[2:4] </code>
Нарезка не копирует данные среза. Она создаёт новое значение среза, которое указывает на исходный массив. Это делает операции с срезами такими же эффективными, как и манипуляции с индексами массива. Следовательно, изменение элементов (не самого среза) пересрезанного среза изменяет элементы исходного среза:
Ранее мы создавали срез s с длиной меньше, чем его вместимость. Увеличить s до его вместимости можно, снова создав срез:
<code>s = s[:cap(s)] </code>
Срез нельзя увеличить beyond его вместимости. Попытка сделать это вызовет panic во время выполнения, так же, как и при обращении к элементу за пределами среза или массива. Аналогично, срезы нельзя "пересрезать" ниже нуля, чтобы получить доступ к элементам массива ранее.
Увеличение срезов (функции copy и append)
Чтобы увеличить вместимость среза, необходимо создать новый,
более большой срез и скопировать содержимое исходного среза в него.
Этот подход используется в реализациях динамических массивов в других языках
за кулисами.
Следующий пример удваивает вместимость s, создавая новый срез,
t, копируя содержимое s в t,
а затем присваивая значение среза t переменной s:
<code>t := make([]byte, len(s), (cap(s)+1)*2) // +1 в случае, если cap(s) == 0
for i := range s {
t[i] = s[i]
}
s = t
</code>
Часть этого общего действия, связанная с циклом, упрощается с помощью встроенной функции copy.
Как следует из названия, copy копирует данные из исходного среза в целевой срез.
Она возвращает количество скопированных элементов.
<code>func copy(dst, src []T) int </code>
Функция copy поддерживает копирование между срезами разной длины
(она скопирует только до меньшего количества элементов).
Кроме того, copy может корректно обрабатывать исходные и целевые срезы,
которые разделяют один и тот же базовый массив,
корректно обрабатывая перекрывающиеся срезы.
Используя copy, мы можем упростить приведённый выше фрагмент кода:
<code>t := make([]byte, len(s), (cap(s)+1)*2) copy(t, s) s = t </code>
Часто возникает необходимость добавить данные в конец среза. Эта функция добавляет элементы типа byte в срез типа byte, увеличивая срез при необходимости, и возвращает обновлённое значение среза:
<code>func AppendByte(slice []byte, data ...byte) []byte {
m := len(slice)
n := m + len(data)
if n > cap(slice) { // если необходимо, перераспределить
// выделить вдвое больше, чем нужно, для будущего роста.
newSlice := make([]byte, (n+1)*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:n]
copy(slice[m:n], data)
return slice
}
</code>
Использовать AppendByte можно так:
<code>p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}
</code>
Функции, подобные AppendByte, полезны, потому что предоставляют полный контроль
над тем, как растёт срез.
В зависимости от характеристик программы,
может быть желательно выделять память небольшими или большими блоками,
или устанавливать верхнюю границу размера перераспределения.
Но большинство программ не нуждаются в полном контроле, поэтому Go предоставляет встроенную функцию append, которая подходит для большинства задач; она имеет сигнатуру
<code>func append(s []T, x ...T) []T </code>
Функция append добавляет элементы x в конец среза s и увеличивает ёмкость среза при необходимости.
<code>a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}
</code>
Чтобы добавить один срез в другой, используйте ... для раскрытия второго аргумента в список аргументов.
<code>a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // эквивалентно "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}
</code>
Поскольку нулевое значение среза (nil) ведёт себя как срез нулевой длины, вы можете объявить переменную среза и затем добавлять в неё элементы в цикле:
<code>// Filter возвращает новый срез, содержащий только
// элементы s, удовлетворяющие fn()
func Filter(s []int, fn func(int) bool) []int {
var p []int // == nil
for _, v := range s {
if fn(v) {
p = append(p, v)
}
}
return p
}
</code>
Возможная «ловушка»
Как упоминалось ранее, повторное нарезание среза (re-slicing) не создаёт копию базового массива. Весь массив будет находиться в памяти до тех пор, пока он не будет больше referenced. Иногда это может привести к тому, что программа будет удерживать все данные в памяти, когда нужно только небольшая их часть.
Например, эта функция FindDigits загружает файл в память и ищет первую группу последовательных цифровых символов,
возвращая их как новый срез.
<code>var digitRegexp = regexp.MustCompile("[0-9]+")
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}
</code>
Этот код работает так, как ожидается, но возвращаемый []byte указывает на массив, содержащий весь файл.
Поскольку срез ссылается на исходный массив, пока срез существует, сборщик мусора не может освободить массив;
несколько полезных байтов файла удерживают в памяти всё содержимое файла.
Чтобы исправить эту проблему, можно скопировать интересующие данные в новый срез перед возвратом:
<code>func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c, b)
return c
}
</code>
Более компактная версия этой функции может быть создана с использованием append.
Это оставлено как упражнение для читателя.
Дополнительное чтение
Effective Go содержит подробное описание срезов и массивов, а спецификация языка Go определяет срезы и их связанные вспомогательные функции.
Следующая статья: JSON и Go
Предыдущая статья: Go: один год назад сегодня
Индекс блога