#8 golang Срезы (слайсы) - "динамические массивы" (контейнеры, произвольная длина)
Primary tabs
Forums:
Срез (или "слайс", в го это одно и то же) — тип данных похожий на массивы, но в сраз можно добавлять данные, используя функцию 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. На следующей строчке добавляется еще один элемент. Стоит обратить внимание на то, что:
- при последнем добавлении произошла переаллокация слайса
- 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)
Работа со строками с помощью срезов
Рассмотрим в отдельной заметке работу со строками с помощью оператора среза и представления их как последовательностей символов или байт.
Что еще почитать
- Слайсы в го-туре: https://go.dev/tour/moretypes/7
- Log in to post comments
- 76 reads
vedro-compota
Thu, 02/26/2026 - 21:06
Permalink
> он не инициализирован,
> он не инициализирован, внутри nil,
-- пояснить
_____________
матфак вгу и остальная классика =)