Menu

Слайсы в Golang: append, make, len и cap, баг общего массива

Слайсы это повседневный тип списка в Go. make, append и рост, len и cap, как срез разделяет базовый массив (и к какому багу это приводит), copy, пакет slices, двумерные слайсы, nil и пустой слайс.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Слайс в одном примере

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

Два способа исправить:

  1. Полное выражение среза s[low:high:max] ограничивает ёмкость значением max-low. Без свободной ёмкости следующий append обязан выделить память. first := base[:2: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, а пустой как [].

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ