#8 golang Срезы (слайсы) - "динамические массивы" (контейнеры, произвольная длина)

Срез (или "слайс", в го это одно и то же) — тип данных похожий на массивы, но в сраз можно добавлять данные, используя функцию append(), причем не надо заранее знать сколько элементов потребуется.


За счет чего достигается динамичность размера - Вместимость и длина

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

Это достигается за счет того, что у слайса есть вместимость (capacity) и длина (len):

  • Вместимость capacity - показывает, какое количество элементов слайс может вместить,
  • а длинафактическое количество элементов, записанных в слайс.

Для начала рассмотрим простые способы создания слайса, а длину и вместимость будем проверять через функции len() и cap().

Объявление среза

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

var buf0 []int // len=0, cap=0
fmt.Printf("len %d cap %d \n", len(buf0), cap(buf0))

-- в данном примере для buf0 мы создаем совершенно пустой слайс. У него не будет ни длины, ни capacity, он не инициализирован, внутри nil, но, тем не менее, с ним уже можно работать, например, добавлять элементы.

Другие варианты объявления/инициаллизации слайсов

Создаем слайс, который уже инициализирован, но элементов в нем по-прежнему нет. Его длина и capacity также равны нулю:

buf1 := []int{} // len=0, cap=0
fmt.Printf("len %d cap %d \n", len(buf1), cap(buf1))

Слайс можно сразу инициализировать значениями:

buf2 := []int{42} // len=1, cap=1
fmt.Printf("len %d cap %d \n", len(buf2), cap(buf2))

-- в данном случае кладем туда одно значение - 42. Под это сразу выделяется слайс размером в 1 элемент, то есть его длина и capacity равны 1.

Инициаллизация среза через make()

Часто для инициализации слайсов встроенная функция make(), которая создает слайс нужного размера и capacity, например:

make([]int, 0) 

-- создает вообще пустой слайс без значений и памяти под него не выделена.
Однако если указать 5 вторым параметром:

buf4 := make([]int, 5) // len=5, cap=5
fmt.Println(buf4) // распечатает [0 0 0 0 0]

-- то уже создается слайс, который будет проинициализирован пятью элементами. Они будут иметь значения по умолчанию. При этом вместимость будет тоже будет = 5.

Задание вместимости при инициализации

Можно сразу указать capacity. Это полезно, если вы знаете, сколько элементов будет в слайсе, например:

buf5 := make([]int, 5, 10) // len=5, cap=10
fmt.Println(buf5)


-- создаем слайс из 5 элементов, но сразу аллоцируете память для десяти. Такой подход положительно сказывается на скорости программы.

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

Обращение к элементам слайса

Обращаться к элементам слайса можно через квадратные скобки, как это работает с массивами. Если вдруг вы обратитесь к элементу, который выходит за границы этого слайса, то выбросится runtime паника.

// обращение к элементам
someInt := buf2[0]

// ошибка при выполнении
// panic: runtime error: index out of range
// someOtherInt := buf2[1]

Добавление элементов в срез

Как писали выше, элементы докидываются с помощью функции append:

var buf0 []int // len=0, cap=0
buf0 = append(buf0, 1)
fmt.Println(buf0) // распечатает [1]

Можно добавлять за раз и более одного элемента:

// добавление элементов
var buf []int         // len=0, cap=0
buf = append(buf, 9, 10) // len=2, cap=2
buf = append(buf, 12) // len=3, cap=4

В слайсе из примера сначала нет ничего, после в слайс добавлено два элемента — 9 и 10. На следующей строчке добавляется еще один элемент. Стоит обратить внимание на то, что:

  1. при последнем добавлении произошла переаллокация слайса
  2. Capacity становится не 3, а 4, потому что при увеличении размерности runtime просто делает x2 от предыдущего размера (т.е. увеличивает зарезервированную память в два раза).

Объединение срезов

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

// добавление другого слайса
otherBuf := make([]int, 3)  // [0,0,0]
buf = append(buf, otherBuf...) // len=6, cap=8

Получение части, куска слайса - с указанием границ

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

buf := []int{1, 2, 3, 4, 5}

// получение среза, указывающего на ту же память
sl1 := buf[1:4] // [2, 3, 4]
sl2 := buf[:2] // [1, 2]
sl3 := buf[2:] // [3, 4, 5]
fmt.Println(sl1, sl2, sl3)
fmt.Printf("sl1: %T\n", sl1)

Получать срез можно не только на базе другого среза (слайса), но и на базе массива:

buf := []int{1, 2, 3, 4, 5}    // слайс
arr := [...]int{1, 2, 3, 4, 5} // массив

// получение среза, указывающего на ту же память

var slArr []int = arr[1:4] // срез из массива
slBuf := buf[1:4]          // срез из слайса

// распечатаем типы:
fmt.Printf("buf: %T\n", buf)     // слайс []int
fmt.Printf("arr: %T\n", arr)     // массив [5]int
fmt.Printf("slBuf: %T\n", slBuf) // слайс []int
fmt.Printf("slArr: %T\n", slArr) // слайс []int
// распечатаем значения:
fmt.Println("buf: ", buf)
fmt.Println("arr: ", arr)
fmt.Println("slBuf: ", slBuf)
fmt.Println("slArr: ", slArr)

-- тут мы используем %T, чтобы распечатать тип переменной

Переаллокация памяти - потеря связи между производным срезом и базовым срезом/массивом

В случае, если очередной append() не умещается во вместимость слайса, происходит повторное (очередное) выделение памяти, и тогда производные слайсы (которые мы указывали через границы на базе основных) перестают ссылаться "на ту же память":

buf := []int{1, 2, 3, 4, 5}    // слайс
arr := [...]int{1, 2, 3, 4, 5} // массив

// получение среза, указывающего на ту же память

var slArr []int = arr[1:4] // срез из массива
slBuf := buf[1:4]          // срез из слайса

// распечатаем типы:
fmt.Printf("buf: %T\n", buf)     // слайс []int
fmt.Printf("arr: %T\n", arr)     // массив [5]int
fmt.Printf("slBuf: %T\n", slBuf) // слайс []int
fmt.Printf("slArr: %T\n", slArr) // слайс []int
// распечатаем значения:
fmt.Println("buf: ", buf)
fmt.Println("arr: ", arr)
fmt.Println("slBuf: ", slBuf)
fmt.Println("slArr: ", slArr)

fmt.Println("-------")
slBuf = append(slBuf, 8) // докинем в срез на срезе
slArr = append(slArr, 8) // докинем в срез на массиве
// увидим, что был затерт элемент в базовых данных:
fmt.Println("buf: ", buf)
fmt.Println("arr: ", arr)
// ну и сами производные срезы изменились:
fmt.Println("slBuf: ", slBuf)
fmt.Println("slArr: ", slArr)

slBuf = append(slBuf, 9) // докинем еще раз
slArr = append(slArr, 9)
// эти ребята не изменятся с предыдущей распечатки
fmt.Println("buf: ", buf)
fmt.Println("arr: ", arr)
// а вот эти уже получат новую область
//	памяти и там будут изменения:
fmt.Println("slBuf: ", slBuf)
fmt.Println("slArr: ", slArr)

-- тут мы видим, что изменения в производном срезе меняют память в базовом срезе или массиве, но только до тех пор, пока в базово объекте "было под это место", если же происходит переаллокация памяти, то пути производного и базового значения расходятся, в силу того, что производное получает новую область памяти.

Копирование слайсов (срезов)

Рассмотрим вопрос копирования слайсов/срезов - как нам получить новую, не зависящую от оригинальное область памяти с теми же значениям

Для копирования слайсов можно использовать функцию copy(), которая возвращает количество скопированных элементов, но при этом мы не можем просто просто объявить пустой слайс и скопировать в него непустой:

buf := []int{1, 2, 3, 4, 5}
// копирование одного слайса в другой
var emptyBuf []int // len=0, cap=0
// неправильно, в пустой слайс копирование не пройдет
copied := copy(emptyBuf, buf) // скопировано элементов = 0
fmt.Println(copied, emptyBuf)

-- правильно будет создать новый слайс такой же емкости (вместимости) и такой же длины, сразу с данными, и уже скопировать в него:

buf := []int{1, 2, 3, 4, 5}
// создаем слайс той же вместимости и длины
newBuf := make([]int, len(buf), len(buf))

copied := copy(newBuf, buf)
fmt.Println(copied, newBuf)
// добавим элемент в новый слайс
// и убедимся, что базовый слайс не изменился:
newBuf = append(newBuf, 8)
fmt.Println(buf)
fmt.Println(newBuf)

Копировать данные можно не только в переменную слайса. Можно использовать и производный срез (область в базовом) как место для помещения копии:

// можно копировать в часть существующего слайса
ints := []int{1, 2, 3, 4}
copy(ints[1:3], []int{5, 6}) // ints = [1, 5, 6, 4]
fmt.Println(ints)

Работа со строками с помощью срезов

Рассмотрим в отдельной заметке работу со строками с помощью оператора среза и представления их как последовательностей символов или байт.

Что еще почитать

vedro-compota's picture

> он не инициализирован, внутри nil,

-- пояснить

_____________
матфак вгу и остальная классика =)