Go: Массивы и слайсы

32 вопросов

1 Чем массив отличается от слайса в Go?

Массив - тип фиксированной длины, например [3]int; при присваивании копируется целиком. Слайс - заголовок (указатель на массив + len + cap); при присваивании копируется только заголовок, данные общие. Слайс можно расширять через append, массив - нет.

Открыть отдельно →
2 Из чего состоит заголовок слайса?

Заголовок слайса содержит: указатель на базовый массив (где начинаются данные), длину len и ёмкость cap. Тип слайса не включает размер; длина и ёмкость могут быть меньше длины базового массива.

Открыть отдельно →
3 Как работает append? Когда выделяется новый массив?

append(s, x) добавляет элементы в конец. Если cap(s) хватает, данные пишутся в тот же массив и увеличивается len. Если нет - выделяется новый массив (обычно с большей ёмкостью), старые данные копируются, возвращается новый слайс. Результат нужно присваивать: s = append(s, x).

Открыть отдельно →
4 По какому алгоритму растёт ёмкость слайса при append?

До Go 1.18: при нехватке cap новый cap обычно удваивался (для маленьких слайсов - быстрее). С Go 1.18 формула изменилась: рост зависит от текущей cap (примерно 2x при малых размерах, затем коэффициент уменьшается). Точная логика в runtime.

Открыть отдельно →
5 Какой размер у заголовка слайса в памяти?

На 64-битной платформе заголовок слайса - 24 байта: указатель (8) + len (8) + cap (8). Сам слайс не содержит элементов - элементы лежат в базовом массиве, на который указывает заголовок.

Открыть отдельно →
6 Как объявить слайс и массив?

Массив: var a [3]int, a := [3]int{1,2,3}. Слайс: var s []int (nil), s := []int{}, s := make([]int, 0, 10), s := a[:] (слайс по массиву). Длина массива - часть типа; у слайса - нет.

Открыть отдельно →
7 Что такое nil-слайс?

Слайс с нулевым значением: указатель nil, len 0, cap 0. var s []int - nil-слайс. Вызов append(s, 1) создаёт новый слайс. len(s) и cap(s) для nil равны 0. Срезы от nil-слайса тоже nil. Nil-слайс можно использовать как пустой (например, JSON сериализует nil slice в null, пустой []int{} - в []).

Открыть отдельно →
8 Какие подводные камни у среза (slicing)?

1) Срез разделяет базовый массив с исходным слайсом - изменение элементов видно в обоих. 2) Append через один слайс может перезаписать элементы другого, если cap общий. 3) Держа срез "хвоста", можно держать в памяти весь исходный массив (утечка). Используйте slices.Clip или копирование при необходимости.

Открыть отдельно →
9 Как объединить два слайса?

s = append(s, s2...) - добавить все элементы s2 в s. Три точки раскрывают слайс в аргументы. Для объединения без изменения исходных: s := append([]T(nil), a...); s = append(s, b...) или предвыделить и copy.

Открыть отдельно →
10 Как удалить элемент из слайса?

По индексу i: s = append(s[:i], s[i+1:]...) - сдвигает хвост, меняет len. Порядок не сохраняется при удалении без сдвига: s[i] = s[len(s)-1]; s = s[:len(s)-1]. Оба способа могут оставлять ссылку на базовый массив (влияние на GC). Для обнуления удаляемого элемента (указатели): s[len(s)-1] = nil.

Открыть отдельно →
11 Какая сложность поиска в неотсортированном слайсе?

Линейная O(n): перебор элементов. Для отсортированного слайса - бинарный поиск: sort.SearchInts(a, x) или sort.Search, сложность O(log n). Пакет slices (Go 1.21+): slices.Contains, slices.Index - линейный поиск.

Открыть отдельно →
12 Какая сложность удаления элемента с начала слайса?

Удаление с начала через s = s[1:] - O(1) по времени, но сдвигает указатель по базовому массиву; весь массив остаётся в памяти до сборки мусора. Удаление через append(s[:0], s[1:]...) копирует хвост - O(n). Для частого удаления с начала рассмотрите очередь на кольцевом буфере или списке.

Открыть отдельно →
13 Какая сложность append в конец?

В среднем O(1) амортизированно: при достаточной cap - просто запись и увеличение len. При нехватке cap - выделение нового массива и копирование - O(n), но происходит редко, поэтому амортизированная стоимость добавления одного элемента - константа.

Открыть отдельно →
14 Для чего нужна функция copy?

copy(dst, src) копирует элементы из src в dst; копируется минимум из len(dst) и len(src). Возвращает количество скопированных элементов. Не меняет len/cap слайсов. Используется для клонирования слайса, сдвига в пределах одного слайса (copy(s[:i], s[i+1:])) и т.д.

Открыть отдельно →
15 Потокобезопасен ли слайс при записи из разных горутин?

Нет. Одновременная запись (включая append) из нескольких горутин - data race. Чтение параллельно с записью - тоже race. Нужна синхронизация: мьютекс, канал или использование одного писателя. Параллельное чтение без записи безопасно.

Открыть отдельно →
16 Изменяется ли слайс, если его передать в функцию и там сделать append?

Заголовок слайса передаётся по значению. Если в функции делают append, может выделиться новый массив и в локальную копию заголовка записывается новый указатель - вызывающий свой слайс не видит. Чтобы изменить слайс снаружи, передают указатель на слайс *[]T или возвращают новый слайс из функции и присваивают: s = append(s, x) и вернуть s.

Открыть отдельно →
17 Как передаётся массив и слайс в функцию?

Массив передаётся по значению - копируется целиком. Большие массивы лучше передавать по указателю *[N]T или передавать слайс по этому массиву. Слайс передаётся по значению (копируется заголовок), поэтому изменение элементов внутри функции видно снаружи, но изменение самого слайса (len/cap/указатель) снаружи не видно.

Открыть отдельно →
18 Как отсортировать слайс? sort.Slice?

sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) сортирует слайс по заданной функции "меньше". sort.SliceStable - стабильная сортировка. Для срезов примитивов есть sort.Ints, sort.Strings и т.д. Пакет slices (Go 1.21+): slices.Sort, slices.SortFunc.

Открыть отдельно →
19 Что происходит при append, когда len == cap?

Выделяется новый массив с большей ёмкостью (по правилам роста runtime), в него копируются старые элементы, добавляется новый, возвращается слайс с указателем на новый массив. Старый массив перестаёт быть доступен по этому слайсу и может быть собран GC.

Открыть отдельно →
20 Что означает nil для слайса? Отличие от пустого слайса?

Nil-слайс: указатель nil, len 0, cap 0. Пустой слайс s := []int{} или make([]int, 0) - ненулевой указатель (может указывать на пустой или фиктивный массив), len 0, cap 0. Для len, append, range поведение одинаковое. Отличие проявляется при сериализации (nil -> null в JSON) и при сравнении с nil.

Открыть отдельно →
21 Можно ли итерировать nil-слайс?

Да. for range по nil-слайсу выполнит ноль итераций. len(s) для nil равен 0. Итерировать по nil-слайсу безопасно и идиоматично.

Открыть отдельно →
22 Где в памяти лежат элементы слайса?

Элементы лежат в базовом массиве в куче (или на стеке, если компилятор доказал, что срез не убегает). Заголовок слайса хранит указатель на первый элемент этого массива. Несколько слайсов могут разделять один массив (разные срезы одного массива или один и тот же слайс).

Открыть отдельно →
23 В чём разница len и cap?

len(s) - количество доступных элементов (индексы 0..len-1). cap(s) - ёмкость: от начала слайса до конца базового массива. Всегда cap >= len. Append может расти без реаллокации, пока len < cap.

Открыть отдельно →
24 Как вставить элемент в середину слайса?

s = append(s[:i], append([]T{x}, s[i:]...)...) - вставка x на позицию i (с лишней аллокацией промежуточного слайса). Без лишней аллокации: s = append(s, 0); copy(s[i+1:], s[i:]); s[i] = x. Либо сдвиг через copy и запись в s[i].

Открыть отдельно →
25 Как задать начальную длину и ёмкость слайса?

make([]T, len, cap) - слайс длины len, ёмкости cap. Элементы - zero value типа T. make([]T, n) - len и cap равны n. Если cap опущен, он равен len. Предвыделение cap уменьшает реаллокации при последующих append.

Открыть отдельно →
26 Что такое трёхиндексный срез (three-index slice)?

Синтаксис s[low:high:max]: ограничивает cap результирующего слайса значением max - low. Дальнейший append не будет перезаписывать элементы исходного слайса за пределами [low:high]. Используется для "отрезания" слайса без общей ёмкости с хвостом.

Открыть отдельно →
27 Как возможна утечка памяти из-за среза (subslice)?

Если сохранить "хвост" большого слайса tail := big[1000:], в tail хранится указатель на тот же массив. Весь массив не будет собран GC, пока доступен tail, даже если первые 1000 элементов больше не нужны. Решение: скопировать нужное в новый слайс или использовать slices.Clip (Go 1.21+), чтобы уменьшить cap.

Открыть отдельно →
28 Что делают slices.Clip и slices.Grow?

slices.Clip(s) (Go 1.21+) уменьшает cap до len, отвязывая срез от "лишнего" хвоста базового массива - помогает избежать утечек. slices.Grow(s, n) возвращает слайс с гарантированной дополнительной ёмкостью для n элементов (для последующего append без реаллокации).

Открыть отдельно →
29 Как сравнить два слайса? slices.Equal?

slices.Equal(a, b) (Go 1.21+) сравнивает элементы поэлементно; для сравнимых типов - то, что нужно. Для своих типов - slices.EqualFunc. Раньше использовали reflect.DeepEqual (медленнее и сравнивает "глубоко", в том числе типы).

Открыть отдельно →
30 Как отсортировать слайс структур по полю?

sort.Slice(s, func(i, j int) bool { return s[i].Field < s[j].Field }). Стабильная сортировка: sort.SliceStable. В Go 1.21+: slices.SortFunc(s, func(a, b T) int { return cmp.Compare(a.Field, b.Field) }) с пакетом cmp.

Открыть отдельно →
31 Nil слайс и пустой слайс - одно и то же?

Нет. Nil - нулевое значение типа слайса (указатель nil). Пустой - len 0, но ненулевой указатель (например, []int{}). Поведение при len, range, append одинаковое. Отличие: сериализация (nil -> null), сравнение с nil, иногда проверки в коде.

Открыть отдельно →
32 Что делает clear(s) для слайса? (Go 1.21+)

Встроенная функция clear(s) обнуляет все элементы слайса в диапазоне [0:len(s)] (zero value для типа элемента). Cap не меняется. Для слайса указателей полезно обнулить элементы, чтобы GC мог собрать данные. Для map clear(m) удаляет все пары.

Открыть отдельно →
🧠Квиз 🏆Лидеры 🎯Собесед. 📖Вопросы 📚База зн.