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>
Нарезка не копирует данные среза. Она создаёт новое значение среза, которое указывает на исходный массив. Это делает операции с срезами такими же эффективными, как манипуляции с индексами массива. Поэтому изменение элементов (не самого среза) нового среза приводит к изменению элементов исходного среза:
d := []byte{'r', 'o', 'a', 'd'} e := d[2:] // e == []byte{'a', 'd'} e[1] = 'm' // e == []byte{'a', 'm'} // d == []byte{'r', 'o', 'a', 'm'}Ранее мы создавали срез s с длиной меньше, чем его вместимость. Увеличить s до его вместимости можно, снова создав срез:
<code>s = s[:cap(s)] </code>
Срез нельзя увеличить больше своей вместимости. Попытка сделать это приведёт к ошибке времени выполнения, аналогично индексации за пределами среза или массива. Аналогично, срезы нельзя пересоздать с отрицательным индексом, чтобы получить доступ к элементам массива до текущего начала.
Увеличение срезов (функции 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: один год назад сегодня
Индекс блога