Массивы и слайсы

Массив

Массив - это набор элементов одного типа, имеющий фиксированный размер. Занимает непрерывный участок в памяти. Определяется длиной и типом его элементов.

Например, тип [4]int представляет массив из четырёх целых чисел. Представление [4]int в памяти - это просто четыре целых значения, расположенных последовательно:

Размер массива неизменяем; его длина - это часть его типа ([4]int и [5]int разные, несовместимые типы). Массивы могут быть проиндексированы, поэтому с помощью выражения s[n] мы получаем доступ к n-ному элементу, начиная с нуля. Массивы не нужно инициализировать явно; нулевой массив - это готовый к использованию массив, элементы которого являются нулями:

var a [4]int
a[0] = 1 // a[2] == 0, нулевое значение типа int

При попытке обращения к элементу за пределами диапазона значений(наприме a[5] в данном случае, программа не скомпилируется.

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

Массив можно инициализировать, как указывая размер так и можно указать компилятору посчитать количество значений:

b := [2]string{"Penn", "Teller"}
с := [...]string{"Penn", "Teller"}

Слайсы

Слайс - это тип, производный от массива. Похож на динамичесие массивы из других языков программирования. Он состоит из указателя на первый элемент базового массива, length и capacity

type slice struct {
	array unsafe.Pointer //первый элемент слайса в массиве
	len   int //длина
	cap   int //вместимость
}

Переменная s, созданная с помощью make([]byte, 5), имеет такую структуру:

Length - это число элементов, на которое ссылается слайс. Capacity - это число элементов лежащего в основе массива (начиная с элемента, на который ссылается указатель слайса). Разница между длиной и вместимостью станет чётче по ходу знакомства с остальными примерами.

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

s = s[2:4]

Слайсниг не производит копирование данных слайса. Создаётся новое значение слайса, указывающее на исходный массив. Это делает операцию слайсинга такой же эффективной, как и манипуляции с индексами массива. Таким образом, изменение элементов (не самого слайса) нового слайса изменяет элементы исходного:

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 до её вместимости, сделав слайсинг снова. Слайс нельзя сделать большим, чем его вместимость. Если вы попытаетесь, это вызовет панику времени выполнения, как и когда происходит обращение к индексу вне границ слайса или массива.

При попытке обратиться к несуществующему элементу слайса, ловим панику.

Проверяем слайс на пустоту с помощью len(list) == 0, а не сравнивая с nil

Добавление элементов в слайc - append

Элементы в слайс принято добавлять с помощью встроенной функции append. Обратите внимание, что после работы функции, может измениться базовый массив. Если внутри функции становится ясно, что capacity недостаточен, для того, чтобы расширить длину слайса и добавить новые элементы, то создастся новый базовый массив, куда скопируются данные из старого + добавятся новые.

Размер нового массива будет х2 от старого, если он был меньше 1024кб или увеличиться на четверть, если массив уже достиг 1024кб.

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

Если знаем размер заранее, нужно всегда аллоцировать память для слайса, так мы избежим пересоздание массива каждый раз когда мы упираемся в его capacity.

Копирование слайсов - copy

copy. Как подсказывает её имя, эта функция копирует данные из слайса-источника в слайс-приёмник. Возвращается количество скопированных элементов. Первый параметр dist - куда копируем, второй src - откуда.

func copy(dst, src []T) int

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

t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t

Возможная ловушка memory leak

Как говорилось ранее, переслайсинг (re-slicing) среза не создаёт копию массива в основании. Массив полностью будет существовать в памяти, пока на него не перестанут ссылаться. Иногда это вызывает хранение всех данных в памяти, когда нужна только их небольшая часть.

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

var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    return digitRegexp.Find(b)
}

Этот код работает, как и говорилось, однако возвращаемый срез []byte указывает на массив, содержащий файл целиком. Так как слайс ссылается на исходный массив, пока слайс есть в памяти, сборщик мусора не сможет очистить массив; несколько важных байтов файла держат всё содержимое в памяти.

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

func CopyDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    b = digitRegexp.Find(b)
    c := make([]byte, len(b))
    copy(c, b)
    return c
}