Как слайсы работают


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

Слайсы — не самостоятельная структура хранения, а описание части массива, включающее метаинформацию: длину и вместимость (capacity). Они используются повсеместно — в функциях, структуре данных, обработке входных и выходных данных.

1. Что такое слайс

Слайс в Go устроен как структура:

type sliceHeader struct {
Data uintptr // указатель на первый элемент в массиве
Len int // количество доступных элементов
Cap int // вместимость от Data до конца массива
}

Реальный тип: []T, где T — тип элементов.

Пример:

arr := \[5\]int{10, 20, 30, 40, 50}
s := arr\[1:4\] // содержит элементы: 20, 30, 40
  • len(s) = 3

  • cap(s) = 4 (от arr[1] до конца arr)

2. Срезы и массивы

Слайсы не хранят данные — они ссылаются на массив. Несколько слайсов могут ссылаться на один и тот же массив:

a := \[5\]int{1, 2, 3, 4, 5}
s1 := a\[1:4\]
s2 := a\[2:\]
s1\[1\] = 99 // изменяет a\[2\], s2 тоже увидит это изменение

Таким образом, слайсы передают ссылку на данные, а не копию.

3. Создание слайсов

3.1. С помощью литерала

s := \[\]int{1, 2, 3}

Создаётся массив и слайс, ссылающийся на него.

3.2. С помощью make

s := make(\[\]int, 5) // len=5, cap=5
s := make(\[\]int, 3, 10) // len=3, cap=10

make создаёт массив нужной длины и возвращает слайс.

3.3. Из массива

arr := \[5\]int{1, 2, 3, 4, 5}
s := arr\[1:4\] // элементы 2, 3, 4

4. len и cap

  • len(slice) — количество элементов в слайсе.

  • cap(slice) — сколько элементов можно получить от начала до конца массива без перераспределения памяти.

a := \[5\]int{1, 2, 3, 4, 5}
s := a\[1:3\] // len=2, cap=4

5. Слайс как динамический массив

При добавлении элементов с помощью append, Go может перераспределить память, если не хватает cap:

s := \[\]int{1, 2, 3}
s = append(s, 4) // len=4, cap может увеличиться

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

6. Механизм append

s := \[\]int{1, 2, 3}
s2 := append(s, 4, 5)

Алгоритм append:

  1. Проверяет cap.

  2. Если cap >= len + n, расширяет len и записывает элементы.

  3. Иначе:

    • выделяет новый массив;

    • копирует старые элементы;

    • добавляет новые;

    • возвращает новый слайс.

Важно: append может создать новый слайс. Старый и новый могут указывать на разные массивы.

7. Copy и sharing данных

s1 := \[\]int{1, 2, 3}
s2 := s1 // ссылаются на один массив
s3 := make(\[\]int, 3)
copy(s3, s1) // копирует данные, s3 теперь независим

Функция copy(dst, src) копирует элементы поэлементно. Полезно, если нужно избежать совместного владения памятью.

8. Слайсы и функции

Слайсы передаются по значению, но содержат ссылку на данные:

func modify(s \[\]int) {
s\[0\] = 100 // изменит оригинал
s = append(s, 200) // это будет новая копия
}

Изменения данных внутри слайса будут видны, но если append приводит к копии — это будет новая память.

9. Слайсы с нулевой длиной и nil

var s \[\]int // nil-слайс
s2 := \[\]int{} // пустой, но не nil
Свойство var s []int []int{}
s == nil true false
--- --- ---
len(s) 0 0
--- --- ---
cap(s) 0 0
--- --- ---

10. Обрезка слайса (резка)

s := \[\]int{1, 2, 3, 4, 5}
sub := s\[1:4\] // элементы: 2, 3, 4

Формат [low : high]:

  • s[low:high] включает индексы от low до high-1.

  • len = high - low

  • cap = исходная_cap - low

Можно также делать:

s := arr\[low:high:max\]

Где max ограничивает capacity результата.

11. Реалокация и поведение append

s1 := \[\]int{1, 2, 3}
s2 := append(s1, 4)
s1\[0\] = 99
fmt.Println(s2\[0\]) // может быть 99 или 1 в зависимости от cap(s1)

Если cap(s1) >= 4, то append не создаёт новый массив — s2 и s1 указывают на один и тот же массив.

Если cap(s1) < 4 — создаётся новый массив, s2 будет независимым.

12. Рост capacity

Алгоритм роста cap при append:

  • При маленьких слайсах: cap *= 2.

  • При больших: рост может быть менее агрессивным.

  • В Go 1.19+ рост может быть нелинейным для больших слайсов.

13. Удаление элемента

В Go нет встроенной функции remove. Можно сделать вручную:

s := \[\]int{1, 2, 3, 4}
i := 2
s = append(s\[:i\], s\[i+1:\]...)

Удаляет элемент с индексом i.

14. Слайсы и zero value

Ноль значения []T — это nil. Это корректный слайс, можно выполнять append, len, range:

var s \[\]int
s = append(s, 1) // OK

15. Итерация

for i, v := range s {
fmt.Println(i, v)
}

Можно также использовать обычный for:

for i := 0; i < len(s); i++ {
fmt.Println(s\[i\])
}

16. Слайсы внутри структур

Слайсы можно использовать как поля:

type Buffer struct {
data \[\]byte
}

Поскольку слайс содержит указатель, передача структуры по значению передаёт копию слайда, но не самих данных — будьте осторожны при модификации.

17. Слайсы и интерфейсы

Интерфейсы можно использовать для представления слайсов любого типа, но:

var x interface{} = \[\]int{1,2,3}
fmt.Println(x.(\[\]int)) // type assertion

18. Обходные техники и оптимизация

  • Использование [:0] для повторного использования памяти:
buf := make(\[\]byte, 0, 1024)
buf = buf\[:0\] // обнуляем длину, не выделяя память заново
  • Использование copy вместо append при объединении:
copy(dst\[len(dst):\], src)
  • Предварительное выделение cap для избежания аллокаций:
s := make(\[\]int, 0, 1000)

19. Безопасность при передаче в другие горутины

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

20. Сравнение с другими языками

  • В отличие от Python и JavaScript, слайсы Go — низкоуровневые и эффективные, ближе к std::vector в C++.

  • Не поддерживают встроенные методы как filter, map, reduce, но легко реализуются вручную.