The Go Blog

Go Slices: usage and internals

Andrew Gerrand
5 January 2011

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: один год назад сегодня
Индекс блога

GoRu.dev Golang на русском

На сайте представлена адаптированная под русский язык документация языка программирования Golang