Что такое срез (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]

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

Ответ 18+ 🔞

А, срезы в Go, ёпта! Ну это ж просто песня, а не структура данных, если разобраться. Сидит себе такой лёгкий заголовочек, как манда с ушами, и управляет целым массивом, блядь!

Смотри, представляешь: есть у тебя массив — здоровенный, неповоротливый, как пьяный медведь в берлоге. А срез — это как хитрая жопа, которая говорит: «Я тут только кусочек возьму, но если что — развернусь на весь базовый массив, нахуй!»

Вот что у него внутри, в этой обманке:

// Псевдокод, но суть пиздец как важна
type sliceHeader struct {
    Data unsafe.Pointer // 1. Тыкает пальцем в массив: «Вот мой!»
    Len  int            // 2. Сколько я сейчас реально использую
    Cap  int            // 3. А сколько я МОГУ использовать, если разойдусь!
}
  • len (длина): Сколько элементов ты сейчас видишь. Спросишь len(s) — он тебе честно ответит.
  • cap (ёмкость): А это его потаённые резервы, блядь! Сколько места есть в том массиве, на который он тычет, начиная с его точки. cap(s) — и ты в курсе, насколько он может распиздеться.

Что он умеет, этот пройдоха:

  1. Появиться на свет:

    • Сразу с данными: s1 := []int{1, 2, 3} (родился с тремя, ёмкость тоже три — скромник).
    • Пустой, но амбициозный: s2 := make([]int, 5, 10) («Дай-ка мне массив на 10 мест, но пока я займу только 5, буду прикидываться маленьким»).
    • Откусить от чужого: s3 := arr[1:4] (Вот это классика! «А я от этого большого массива вот этот кусочек себе возьму»).
  2. append (жадное добавление): slice = append(slice, elem1, elem2, ...) Тут начинается цирк, ёперный театр!

    • Если место есть (len < cap): Подходит, тихонечко кладёт новый элемент в свой базовый массив. Len увеличил — и всё, доволен, как слон. Массив тот же.
    • Если место кончилось (len == cap): О, тут начинается пиздец! Он такой: «Всё, накрылся медный таз!». Идёт переаллокация — находит новый, больший массив (обычно в два раза больше, жадная свинья), перетаскивает туда все свои старые пожитки, добавляет новое, и теперь тыкает пальцем уже в ЭТОТ новый массив. Старый — пошёл нахуй, прости-прощай.
    • Запомни, кретин! append возвращает НОВЫЙ заголовок. Ты обязан сказать s = append(s, ...), а не просто append(s, ...). Иначе он там себе новый создаст, а ты со старым будешь сидеть и удивляться, хули ничего не добавляется.
  3. Slicing (поделиться... или сделать вид): newSlice := oldSlice[low:high] Вот тут самый подвох, блядь! Это не копирование, нет! Это как дать другу посмотреть в свою подзорную трубу, но в тот же самый лес.

    • newSlice смотрит на тот же самый базовый массив, что и oldSlice.
    • Просто начинает с другого места (low) и видит другую длину (high - low).
    • А ёмкость у него будет cap(oldSlice) - low. То есть он может видеть ещё дальше, чем старый, если захочет!

Ловушка, в которую все попадают: общий массив

Несколько срезов могут тыкать своими грязными пальцами в один и тот же массив. И если один начнёт там ковыряться — другому прилетит, сам от себя охуеешь!

original := []int{10, 20, 30, 40, 50}
s1 := original[1:3] // {20, 30}, видит 2, но может увидеть ещё 40 и 50!
s2 := original[2:5] // {30, 40, 50}, видит 3

fmt.Println(s1, s2) // [20 30] [30 40 50] — пока всё мирно

s1[1] = 99 // s1 такой: «А давайте-ка я тут 30 на 99 поменяю!»

// БАБАХ! Изменение полетело по всей цепи, как волна ебушки-воробушки!
fmt.Println(original) // [10 20 99 40 50] — оригинал поплыл
fmt.Println(s2)       // [99 40 50]       — и бедный s2 уже видит 99 вместо 30!

Вот так-то, дружок. Срез — он как кот, который гуляет сам по себе, но если его неправильно понять, он тебе всю программу обосрёт. Красота, блядь!