Слайс в одном примере
Слайс это растущее окно в массив. Его создают литералом или через make, увеличивают через append, а размер узнают через len.
У []string нет длины в скобках. Этим он и отличается от типа массива вроде [4]string, размер которого фиксирован.
Что такое слайс
Значение слайса это небольшой заголовок из трёх полей:
- указатель на элемент базового массива
- длина: сколько элементов видит слайс
- ёмкость: сколько элементов есть от этого указателя до конца базового массива
Копирование слайса (присваивание, передача в функцию) копирует только этот заголовок. Обе копии указывают на одни и те же элементы. Почти все сюрпризы со слайсами происходят, когда об этом забывают.
make: длина и ёмкость
make([]T, len, cap) выделяет базовый массив и возвращает слайс поверх него. Ёмкость необязательна и по умолчанию равна длине.
Частая путаница: make([]int, 5) и затем пять вызовов append дают десять элементов, первые пять из которых нули. Используйте make([]int, 0, 5), если собираетесь добавлять через append, или make([]int, 5), если собираетесь присваивать по индексу.
Предварительное выделение ёмкости, когда итоговый размер известен, избавляет от повторных ростов. Это оптимизация, а не требование.
append и рост
append добавляет элементы после текущей длины. Если ёмкости хватает, запись идёт на месте. Если нет, выделяется массив большего размера, старые элементы копируются в него, и возвращается слайс, указывающий на новый массив.
Пока слайс маленький, ёмкость примерно удваивается, а после 256 элементов растёт с меньшим коэффициентом (приближаясь к 1.25x). Точные числа это деталь реализации среды выполнения, и они менялись между версиями Go, так что никогда не пишите код, который от них зависит.
Поскольку append может вернуть другой массив, её результат нужно использовать:
append(s, 4) // compile error: append(s, 4) (value of type []int) is not used
s = append(s, 4) // correct
Добавление целого слайса использует синтаксис распаковки: s = append(s, other...).
Срез разделяет базовый массив
s[low:high] создаёт новый заголовок слайса над тем же массивом, охватывающий индексы от low до high, не включая high. Любую границу можно опустить. Элементы не копируются.
Вывод:
[10 99 30 40 50]
3 4
Именно это разделение делает срезы дешёвыми. И оно же источник самого знаменитого бага со слайсами.
Баг с общим массивом при append
Когда у подслайса есть свободная ёмкость, append в него пишет в массив родителя, поверх элементов, которые родитель ещё использует.
Вывод:
[1 2 100 4 5]
[1 2 100]
[cmd test] [cmd test]
a и b делят ячейку массива после "cmd", поэтому второй append перезаписал первый. Код выглядит нормально и работает, когда ёмкость случайно заполнена, поэтому баг проявляется лишь иногда.
Два способа исправить:
- Полное выражение среза
s[low:high:max]ограничивает ёмкость значениемmax-low. Без свободной ёмкости следующийappendобязан выделить память.first := base[:2:2]делает первый пример безопасным. - Явное копирование, когда слайс переживёт вызов или в него будут добавлять независимо:
slices.Clone(prefix)илиappend([]string(nil), prefix...).
Связанная ловушка: маленький слайс огромного массива удерживает весь массив от сборщика мусора. Если прочитать файл на 100 МБ и сохранить data[:10], все 100 МБ останутся в памяти. Клонируйте ту часть, которую сохраняете.
copy
Встроенная copy(dst, src) копирует min(len(dst), len(src)) элементов и возвращает это число. dst она никогда не увеличивает.
copy корректно обрабатывает перекрывающиеся источник и приёмник, поэтому copy(s[1:], s) сдвигает элементы вправо без порчи данных.
Пакет slices
Начиная с Go 1.21 в стандартном пакете slices есть обобщённые функции для операций, которые раньше писали вручную.
| Функция | Что делает |
|---|---|
Contains, Index | ищут значение (ContainsFunc, IndexFunc принимают предикат) |
Sort, SortFunc, SortStableFunc | сортируют на месте |
BinarySearch | ищет в отсортированном слайсе |
Insert, Delete, DeleteFunc | вставляют или удаляют, возвращают новый слайс |
Compact | удаляет идущие подряд дубликаты |
Equal, Compare | сравнивают поэлементно |
Clone, Reverse, Max, Min | делают то, что написано |
Collect, Sorted, Values, All | работают с итераторами (Go 1.23) |
Delete и Insert возвращают слайс, который нужно присвоить, как и append. Сортировке посвящена отдельная страница: сортировка.
Удаление элементов в цикле по индексу пропускает элементы. Вместо этого используйте slices.DeleteFunc:
s = slices.DeleteFunc(s, func(n int) bool { return n%2 == 0 }) // drop evens
Двумерные слайсы
Слайс слайсов даёт сетку, строки которой могут быть разной длины. Каждую строку нужно выделять отдельно.
Если забыть внутренний make, каждая строка останется nil, и grid[1][2] = 7 запаникует с index out of range [2] with length 0.
nil-слайсы и пустые слайсы
nil-слайс ведёт себя как пустой для len, cap, range и append, поэтому как нулевое состояние лучше var s []T. Инициализируйте []T{}, только когда разница важна, а это в основном вывод JSON (null или []). Проверяйте пустоту через len(s) == 0, а не s == nil.
Слайсы и функции
Функция, получившая слайс, может менять его элементы, и вызывающий код увидит изменения. Изменить длину слайса вызывающего кода она не может, потому что получила копию заголовка. Функция, которая делает append, должна вернуть новый слайс:
func addAll(s []int, vals ...int) []int {
return append(s, vals...)
}
Поэтому append, slices.Delete и slices.Insert все возвращают слайс.
Частые ошибки
- Игнорирование результата
append. Если выбросить его полностью, код не скомпилируется, ноappend(s, x), присвоенный другой переменной, а не той, которой вы продолжаете пользоваться, это логическая ошибка. - Два append от одной основы. Они могут делить хранилище. Клонируйте основу или используйте полное выражение среза.
make([]T, n), а затемappend. В начале остаются n нулей.- Индекс за границами.
s[len(s)]вызывает панику. Последний элемент этоs[len(s)-1]. - Сравнение слайсов через
==. Компилируется толькоs == nil. Используйтеslices.Equal.
Часто задаваемые вопросы
Чем длина слайса отличается от ёмкости в Go?
len(s) это сколько элементов слайс содержит сейчас. cap(s) это сколько элементов помещается в базовом массиве, начиная с первого элемента слайса. append пишет в свободную ёмкость, если она есть, и выделяет новый массив большего размера, если её нет.
Как добавить элемент в слайс в Go?
Вызовите встроенную append и присвойте результат обратно: s = append(s, x). Можно добавить несколько значений (append(s, 1, 2, 3)) или другой слайс (append(s, other...)). Всегда используйте возвращаемое значение, потому что append может вернуть слайс, указывающий на новый массив.
Как удалить элемент из слайса в Go?
Используйте slices.Delete(s, i, i+1) (Go 1.21): она сдвигает последующие элементы и возвращает укороченный слайс. Начиная с Go 1.22 она ещё и обнуляет освободившиеся ячейки в хвосте. Если порядок не важен, поменяйте элемент местами с последним и обрежьте: s[i] = s[len(s)-1]; s = s[:len(s)-1].
Как проверить, содержит ли слайс значение, в Go?
Используйте slices.Contains(s, v) из стандартного пакета slices (Go 1.21). slices.Index(s, v) возвращает позицию или -1, а slices.ContainsFunc принимает предикат. Для многократных поисков по большим данным постройте мапу.
Чем nil-слайс отличается от пустого слайса в Go?
var s []int это nil; s := []int{} пустой, но не nil. У обоих длина 0, оба работают с len, range и append. Разница проявляется в проверках s == nil и в encoding/json, который кодирует nil-слайс как null, а пустой как [].