Что такое срез (slice) в Go? Опишите его внутреннюю структуру и ключевые операции.

Ответ

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

Внутренняя структура (заголовок среза):

Срез — это легковесная структура (заголовок), которая описывает непрерывный участок базового массива. Она состоит из трёх полей:

// Псевдокод структуры
type sliceHeader struct {
    Data unsafe.Pointer // 1. Указатель на начало базового массива
    Len  int            // 2. Длина (len) - количество видимых элементов
    Cap  int            // 3. Ёмкость (cap) - максимальная длина без переаллокации
}
  • len (длина): Количество элементов, которые содержит срез. Доступно через len(s).
  • cap (ёмкость): Количество элементов в базовом массиве, начиная от указателя среза. Доступно через cap(s).

Ключевые операции:

  1. Создание среза:

    • Литерал: s1 := []int{1, 2, 3} (len=3, cap=3)
    • make: s2 := make([]int, 5, 10) (len=5, cap=10)
    • Из массива или другого среза: s3 := arr[1:4]
  2. append (добавление элементов):

    slice = append(slice, elem1, elem2, ...)

    • Если len < cap: Новый элемент добавляется в базовый массив, len увеличивается. Базовый массив не меняется.
    • Если len == cap: Происходит переаллокация. Go создает новый, больший массив (обычно с удвоенной ёмкостью), копирует в него все элементы из старого, добавляет новый элемент и обновляет указатель в заголовке среза.
    • Важно: append возвращает новый заголовок среза, который необходимо присваивать обратно: s = append(s, ...).
  3. Slicing (создание нового среза из существующего):

    newSlice := oldSlice[low:high]

    • newSlice будет использовать тот же базовый массив, что и oldSlice.
    • len(newSlice) будет high - low.
    • cap(newSlice) будет cap(oldSlice) - low.

Важный нюанс: общий базовый массив

Поскольку несколько срезов могут ссылаться на один и тот же базовый массив, изменение элементов в одном срезе может затронуть другой.

original := []int{10, 20, 30, 40, 50}
s1 := original[1:3] // {20, 30}, len=2, cap=4
s2 := original[2:5] // {30, 40, 50}, len=3, cap=3

fmt.Println(s1, s2) // [20 30] [30 40 50]

s1[1] = 99 // Изменяем элемент {20, 99}

// Изменение отразилось в original и s2, так как массив общий
fmt.Println(original) // [10 20 99 40 50]
fmt.Println(s2)       // [99 40 50]

Это критически важная особенность, которую нужно понимать, чтобы избежать неожиданного поведения.