Перейти к содержимому

Слайсы и массивы

Массив в Go — это значение фиксированной длины, причём длина входит в тип: [3]int и [4]int — разные типы. Массив копируется целиком при присваивании и при передаче в функцию, его размер известен компилятору, и он спокойно живёт на стеке, если не убегает. Именно поэтому напрямую массивами в прикладном коде почти не пользуются: они неудобны как параметр функции и не растут.

Слайс — это не контейнер, а дескриптор окна над массивом: структура из трёх машинных слов {ptr *T, len int, cap int} (см. runtime.slice в runtime/slice.go и reflect.SliceHeader). На 64-битной платформе это 24 байта. Слайс всегда передаётся по значению — копируется заголовок, но не данные. Отсюда двойственное поведение, которое и составляет 80% вопросов на собеседовании: изменение элемента через копию слайса видно всем, кто смотрит на тот же массив, а изменение самого слайса (append, переприсваивание, реслайсинг) видно только владельцу конкретной копии заголовка.

append — единственная операция, которая может «отвязать» слайс от исходного массива. Пока len < cap, append пишет прямо в существующий массив и возвращает заголовок с увеличенным len; как только места не хватает, runtime (growslice) выделяет новый массив, копирует туда данные и возвращает заголовок с новым указателем. Из-за этого append иногда мутирует чужие данные, а иногда молча перестаёт это делать — и именно поэтому результат append обязательно нужно присваивать.

Строка близка к слайсу, но проще и жёстче: это два слова {ptr *byte, len int} и байты за указателем неизменяемы. Строка — это UTF-8 байты, индексация даёт байт, range декодирует руны, []byte(s) и string(b) в общем случае копируют. Массив/слайс/строка вместе образуют «треугольник», про который спрашивают в одном вопросе почти всегда.

Коротко. Массив — значение фиксированной длины, длина часть типа, копируется целиком. Слайс — заголовок из трёх слов (указатель, len, cap) поверх массива: длина динамическая, копирование заголовка не копирует данные.

Глубже. Практические следствия: массивы сравнимы через == (если сравним тип элемента), слайсы — нет (только == nil); массив можно использовать как ключ мапы, слайс — нельзя; массив как параметр функции копируется, поэтому функция не видит изменений вызывающего, а слайс делит backing array и изменения элементов видны. У массива нет append, у слайса есть, и он может сменить backing array.

a := [3]int{1, 2, 3}
b := a // полная копия 24 байт данных
b[0] = 99 // a[0] == 1
s := []int{1, 2, 3}
t := s // копия заголовка, тот же массив
t[0] = 99 // s[0] == 99

Как работает встроенная функция append в Go? Что происходит с capacity?

Заголовок раздела «Как работает встроенная функция append в Go? Что происходит с capacity?»

Коротко. Если len(s)+k <= cap(s), append пишет элементы в существующий backing array и возвращает заголовок с увеличенным len — аллокации нет. Иначе runtime выделяет новый массив большего размера, копирует туда старые данные, дописывает новые и возвращает заголовок с новым указателем.

Глубже. Рост считает runtime.growslice. С Go 1.18 правило такое: если требуемая длина больше удвоенной старой ёмкости — берётся сразу требуемая; иначе при oldCap < 256 ёмкость удваивается, а при oldCap >= 256 растёт по формуле newcap += (newcap + 3*256) / 4 в цикле, то есть коэффициент плавно съезжает с 2x к 1.25x. Полученный размер в байтах округляется вверх до класса аллокатора (roundupsize), поэтому фактический cap часто «некруглый». Реальный прогон на Go 1.24 для []int: 256 → 512 → 848 → 1280 → 1792 → 2560. Для []byte с cap 3 последовательность 3 → 8 → 16 → 32 → 64 — тут видно влияние size-классов. Никаких гарантий на точные значения спецификация не даёт: полагаться можно только на cap >= len и на амортизированную константность.

Что изменится, если заменить bar = append(bar, 1) на bar = append(bar, 1, 1) ?

Заголовок раздела «Что изменится, если заменить bar = append(bar, 1) на bar = append(bar, 1, 1) ?»

Коротко. Если в исходном варианте элемент помещался в оставшуюся ёмкость и append писал в чужой backing array, то с двумя элементами ёмкости уже не хватает: выделяется новый массив, и bar перестаёт быть связан с исходным слайсом — «побочный эффект» исчезает.

Глубже. Канонический вид задачи (сам код в вопросе не приведён, восстанавливаю классическую формулировку):

foo := []int{1, 2, 3}
bar := foo[:2] // len=2, cap=3 — общий массив с foo
bar = append(bar, 1) // помещается: foo станет [1 2 1]

Проверено на Go 1.24:

  • append(bar, 1)foo = [1 2 1], bar = [1 2 1], cap=3, указатели совпадают;
  • append(bar, 1, 1) → нужно len=4 > cap=3, аллоцируется новый массив: foo = [1 2 3] (не изменился), bar = [1 2 1 1], cap=6, указатели разные.

Мораль вопроса: наличие или отсутствие видимой мутации соседа зависит не от логики программы, а от арифметики len/cap, поэтому на такое поведение нельзя опираться.

Коротко. То же самое, что и с двумя элементами: foo остаётся [1 2 3], bar уезжает в новый массив, но результат — [1 2 1 1 1], len=5, cap=6.

Глубже. Интересно, что cap в обоих случаях равен 6. Требуемая длина 5 меньше удвоенной старой ёмкости (2·3 = 6), поэтому берётся удвоение → 6 элементов по 8 байт = 48 байт, что ровно попадает в size-класс 48 и округление ничего не меняет. Это хорошая иллюстрация того, что «cap удваивается» — эвристика, а не контракт: результат определяется парой (формула роста, класс аллокатора).

Коротко. Слайс — структура из трёх полей: указатель на первый элемент окна в backing array, длина окна и ёмкость (сколько элементов доступно от указателя до конца массива). 24 байта на 64-битной платформе.

Глубже. В runtime это

runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}

Компилятор знает про эту структуру и генерирует код напрямую: s[i] — это проверка i < len плюс адресная арифметика ptr + i*sizeof(T), len(s)/cap(s) — просто чтение полей, часто вообще без обращения к памяти. Реслайсинг s[a:b:c] создаёт новый заголовок: ptr+a, len=b-a, cap=c-a — данные не копируются. Официально «пощупать» заголовок можно через unsafe.SliceData(s) (Go 1.20+); reflect.SliceHeader объявлен устаревшим и небезопасным.

Что такое «эффект алиасинга» при работе со слайсами и в чем его опасность?

Заголовок раздела «Что такое «эффект алиасинга» при работе со слайсами и в чем его опасность?»

Коротко. Алиасинг — это когда несколько слайсов (или слайс и массив) ссылаются на один и тот же backing array, и запись через один из них незаметно меняет данные, видимые через другой.

Глубже. Типовые источники: b := a, sub := a[1:3], передача слайса в функцию, сохранение слайса в структуре или в мапе. Опасность в том, что поведение недетерминировано с точки зрения читателя кода: пока хватает cap, append в подслайс затирает элементы «родителя», а после превышения cap — уже нет. Второй класс проблем — утечка памяти: head := huge[:10] держит живым весь массив на много мегабайт, пока жив head, потому что GC видит указатель на начало объекта. Третий — гонки: два алиаса в разных горутинах — это data race.

Как можно безопасно работать со слайсом, чтобы избежать этого эффекта?

Заголовок раздела «Как можно безопасно работать со слайсом, чтобы избежать этого эффекта?»

Коротко. Копировать данные там, где нужна независимость (slices.Clone, copy), и ограничивать ёмкость через full slice expression s[a:b:b], чтобы append гарантированно уходил в новый массив.

Глубже. Практический набор правил: для входных слайсов, которые вы сохраняете надолго, делайте slices.Clone(in); при возврате подслайса большого буфера возвращайте bytes.Clone/slices.Clone, а не окно; при выдаче наружу «только для чтения» отдавайте s[:len(s):len(s)], чтобы чужой append не затирал ваш хвост; документируйте, кто владеет слайсом. slices.Clone — это по сути append(S([]T)(nil), s...), то есть nil на входе даёт nil на выходе, а не пустой слайс.

func (c *Cache) Set(key string, val []byte) {
c.m[key] = bytes.Clone(val) // иначе вызывающий переиспользует буфер и сломает кэш
}

Arrays, slices, strings. Internal structure. How they are passed (pointer or value) a) Arrays are immutable b) Slices are mutable; the append function

Заголовок раздела «Arrays, slices, strings. Internal structure. How they are passed (pointer or value) a) Arrays are immutable b) Slices are mutable; the append function»

Коротко. Всё три передаются по значению. Массив — это сами данные (копируются целиком), слайс — заголовок из 3 слов (данные общие), строка — заголовок из 2 слов, и её байты неизменяемы.

Глубже. Формулировка «arrays are immutable» неточна: массив вполне мутабельный (a[0] = 1 работает), просто при передаче в функцию копируется, поэтому вызываемый не видит изменений вызывающего — это иммутабельность по отношению к вызывающему, а не свойство типа. По-настоящему иммутабельна только строка: спецификация запрещает менять её байты, и компилятор/runtime опираются на это (например, строковые константы лежат в read-only секции, s1 == s2 может сравнивать указатели). Слайс мутабелен и «полу-shared»: s[i] = x виден всем алиасам, а s = append(s, x) — только локальной переменной, пока не произойдёт запись в общий массив в пределах cap.

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

Заголовок раздела «Что такое слайс? Чем отличается от массива?»

Коротко. Слайс — динамическое окно (ptr/len/cap) над массивом; массив — значение фиксированного размера, длина которого часть типа. Слайс копируется как заголовок, массив — целиком.

Глубже. См. выше первый вопрос — там же таблица различий по сравнимости и ключам мапы. Добавлю кратко про создание: make([]T, len, cap), литерал []T{...}, реслайс массива arr[:], и [...]T{1,2,3} — это по-прежнему массив, длину которого посчитал компилятор, а не слайс.

Что такое динамический массив, для чего применяются, какое основное назначение и ограничения?

Заголовок раздела «Что такое динамический массив, для чего применяются, какое основное назначение и ограничения?»

Коротко. Динамический массив — непрерывный в памяти буфер с амортизированным O(1) добавлением в конец за счёт геометрического роста ёмкости. В Go его роль играет слайс.

Глубже. Сильные стороны: O(1) доступ по индексу, идеальная локальность (один cache-line подряд), минимум накладных расходов на элемент, дружелюбность к SIMD/memmove. Ограничения: вставка и удаление в середину — O(n); рост требует реаллокации и копирования, то есть отдельная операция может быть O(n) и вызвать паузу/скачок памяти (в пике живут старый и новый массив); указатели/подслайсы, взятые до роста, «отвязываются» от нового массива; удаление элементов не уменьшает ёмкость автоматически, память не возвращается, пока слайс жив.

Где выделяется память под новый массив при расширении?

Заголовок раздела «Где выделяется память под новый массив при расширении?»

Коротко. В куче: growslice вызывает mallocgc. Расширение слайса всегда даёт heap-аллокацию.

Глубже. Изначальный массив может лежать и на стеке — если escape-анализ доказал, что слайс не убегает, и размер известен и невелик. Но growslice не умеет расти на стек: он получает новый объект из аллокатора. Поэтому в горячем цикле полезно заранее задавать make([]T, 0, n) — тогда роста, а значит и heap-мусора, не будет вовсе (при условии, что сам слайс не убегает). Проверять это удобно через go build -gcflags=-m.

Допустим есть слайс из миллиона интов, тогда где он будет создан?

Заголовок раздела «Допустим есть слайс из миллиона интов, тогда где он будет создан?»

Коротко. В куче. Миллион int — это 8 МБ, что заведомо больше лимита компилятора на неявные стековые объекты (порядка 64 КБ для make/new/литералов), поэтому такой backing array всегда уезжает на heap.

Глубже. У компилятора Go два порога: относительно большой для явно объявленных переменных и заметно меньший (64 КБ) для неявных объектов вроде make(...). Плюс стек горутины начинается с 8 КБ и растёт копированием — держать на нём 8 МБ было бы вредно. Сам заголовок слайса (24 байта) при этом вполне может остаться на стеке — на heap уезжают данные, а не дескриптор. Дополнительный нюанс: аллокации больше 32 КБ идут мимо size-классов, напрямую крупными объектами (large object), и make([]int, 1e6) с ненулевой длиной ещё и зануляет 8 МБ, что само по себе заметно в профиле.

Коротко. Тройка {указатель на данные, длина, ёмкость}, описывающая окно над непрерывным массивом; ссылочный по поведению, но передаваемый по значению тип.

Глубже. См. выше «Что такое slice в Go и как он устроен изнутри». Короткая формула для собеседования: «слайс — это не массив, а вид на массив; копия слайса делит данные, но имеет собственные len и cap».

Коротко. Значение фиксированной длины из однотипных элементов, лежащих подряд; длина — часть типа и известна на этапе компиляции.

Глубже. [3]int и [4]int несовместимы, привести одно к другому нельзя. Массив сравним оператором ==, если сравним тип элемента, и может быть ключом мапы. Размер задаётся константным выражением; [...]int{1,2,3} просит компилятор посчитать длину. Начиная с Go 1.17 можно конвертировать слайс в указатель на массив ((*[4]int)(s)), а с Go 1.20 — прямо в массив ([4]int(s)); обе конверсии паникуют, если len(s) меньше нужного.

Коротко. См. выше подробный ответ про append: пишет в пределах cap без аллокации, иначе аллоцирует новый массив с геометрическим ростом и копирует данные. Результат обязательно нужно присвоить.

Глубже. Единственное, что стоит добавить: append — не функция, а встроенная конструкция; компилятор инлайнит быстрый путь (проверка len < cap, запись, инкремент len) и вызывает runtime.growslice только на медленном пути. Поэтому в бенчмарках append с достаточным cap стоит буквально несколько инструкций.

Стандартные вопросы про Go (мапы, слайсы т. д.);

Заголовок раздела «Стандартные вопросы про Go (мапы, слайсы т. д.);»

Коротко. Это не вопрос, а пометка из отчёта о собеседовании: спрашивали базовый блок по структурам данных.

Глубже. Каркас подготовки к такому блоку: слайс (заголовок, append, алиасинг, реслайсинг, утечки), мапа (хеш-таблица, бакеты, отсутствие порядка обхода, непотокобезопасность, невозможность взять адрес элемента, Swiss Tables в Go 1.24), строка (иммутабельность, UTF-8, руны, конверсии), плюс сравнение по стоимости операций. Отвечать лучше сразу «структура в памяти → операции → подводные камни».

Если через рефлексию мы получили Type от значения, переданного через интерфейс, и с помощью .Kind() определили, что это слайс, - как узнать, элементы какого типа содержит этот слайс?

Заголовок раздела «Если через рефлексию мы получили Type от значения, переданного через интерфейс, и с помощью .Kind() определили, что это слайс, - как узнать, элементы какого типа содержит этот слайс?»

Коротко. t.Elem() — возвращает reflect.Type элемента. Метод Elem() определён для Array, Chan, Map, Ptr и Slice.

Глубже.

func elemType(v any) reflect.Type {
t := reflect.TypeOf(v)
if t == nil || t.Kind() != reflect.Slice {
return nil
}
return t.Elem() // для []Point вернёт main.Point
}

Дальше от reflect.Type можно идти вглубь: Elem().Kind(), NumField() для структур, New(t.Elem()) для создания нового элемента, reflect.MakeSlice(t, len, cap) для создания слайса того же типа. Если исходный any был нетипизированным nil, reflect.TypeOf вернёт nil — это надо проверять до Kind(), иначе паника.

Как правильно перебирать с помощью range слайс структур и слайс указателей на структуры эффективно, чтобы не использовать много памяти?

Заголовок раздела «Как правильно перебирать с помощью range слайс структур и слайс указателей на структуры эффективно, чтобы не использовать много памяти?»

Коротко. Для слайса «толстых» структур перебирайте по индексу (for i := range s) и работайте с s[i] или &s[i] — форма for _, v := range s копирует каждый элемент. Для слайса указателей for _, p := range s уже дёшево: копируется одно слово.

Глубже. range со вторым значением всегда делает копию элемента, и для структуры на 200 байт это 200 байт копирования на итерацию плюс потенциальный выход копии в heap, если её адрес утечёт. Индексная форма от этого свободна, и она же единственная позволяет менять элементы на месте:

for i := range items { // items []BigStruct
items[i].Count++ // мутация оригинала, копий нет
}

Важный нюанс версий: до Go 1.21 включительно переменная цикла была одна на весь цикл, и &v / замыкание над v давали классический баг «все элементы одинаковые». С Go 1.22 переменная создаётся заново на каждой итерации, так что &v теперь корректен — но это по-прежнему указатель на копию, а не на элемент слайса, и он вызывает аллокацию. Если нужен указатель на сам элемент — только &s[i].

что такое слайс, а массив? как связаны, особенности аппенда

Заголовок раздела «что такое слайс, а массив? как связаны, особенности аппенда»

Коротко. Массив — данные, слайс — окно над данными; слайс всегда сидит поверх какого-то массива, а append может заменить этот массив на новый, разорвав связь с исходным.

Глубже. Три связки, которые надо назвать: arr[:] делает слайс из массива без копирования; s[a:b:c] делает новое окно поверх того же массива; append — единственная операция, которая может сменить backing array, и делает это только при нехватке cap. Отсюда правило: всегда пишите s = append(s, ...) и никогда не считайте, что после append старые подслайсы «связаны» с новым.

Если в это структуре будет слайс, то он будет полностью скопирован?

Заголовок раздела «Если в это структуре будет слайс, то он будет полностью скопирован?»

Коротко. Нет. При копировании структуры копируется заголовок слайса (24 байта), а backing array остаётся общим — обе копии будут видеть изменения элементов друг друга.

Глубже. Это классический источник багов при «копировании» структур-конфигов и DTO: shallow copy плюс слайс/мапа/канал внутри = скрытое разделение состояния. Deep copy надо писать руками:

func (c Config) Clone() Config {
c.Tags = slices.Clone(c.Tags) // иначе общий массив
c.Extra = maps.Clone(c.Extra) // мапа тоже общая
return c
}

Коротко. См. развёрнутый ответ выше: запись в пределах cap без аллокации, иначе новый массив, копирование, новый указатель; результат нужно присваивать.

Глубже. Отличие от предыдущих формулировок только в акценте на сигнатуру: append(s []T, elems ...T) []T, то есть добавление слайса целиком делается через spread — append(a, b...), а для []byte дополнительно разрешён append(b, "строка"...).

Слайс передается по значению или указателю?

Заголовок раздела «Слайс передается по значению или указателю?»

Коротко. По значению — копируется заголовок из трёх слов. Но так как в заголовке лежит указатель на данные, функция видит и может менять те же элементы.

Глубже. Практический вывод: функция может изменить s[i], и вызывающий это увидит; но append внутри функции, переприсваивание s = ... или изменение длины вызывающий не увидит — у него своя копия заголовка. Если функции надо изменить сам слайс, есть два способа: вернуть новый слайс (идиоматично, как делает сам append) или принять *[]T (используется редко, например в sort-подобных API или при работе с буферами).

func fill(s []int) { s[0] = 1 } // видно снаружи
func grow(s []int) { s = append(s, 1) } // НЕ видно снаружи
func growP(s *[]int) { *s = append(*s, 1) } // видно

Коротко. Классическое слияние двумя указателями за O(n+m) времени и O(n+m) дополнительной памяти; при равенстве берём элемент из первого слайса, чтобы слияние было стабильным.

Глубже.

func Merge[T cmp.Ordered](a, b []T) []T {
res := make([]T, 0, len(a)+len(b)) // одна аллокация, роста не будет
i, j := 0, 0
for i < len(a) && j < len(b) {
if a[i] <= b[j] {
res = append(res, a[i])
i++
} else {
res = append(res, b[j])
j++
}
}
res = append(res, a[i:]...)
res = append(res, b[j:]...)
return res
}

На дополнительные вопросы: слить in-place в a, если в нём есть запас cap, можно, идя с конца (как в известной задаче LeetCode 88); если оба входа — потоки, а не слайсы, задача превращается в k-way merge через container/heap. Если нужен просто отсортированный результат без гарантии линейности, в Go 1.22+ проще написать s := slices.Concat(a, b); slices.Sort(s) — но это O(n log n), и на собеседовании стоит проговорить разницу.

Как устроены слайсы? Что будет при превышении емкости в слайсе? В чем отличие от строки?

Заголовок раздела «Как устроены слайсы? Что будет при превышении емкости в слайсе? В чем отличие от строки?»

Коротко. Слайс — {ptr, len, cap}; при превышении cap внутри append выделяется новый, больший массив и данные копируются (старый слайс/алиасы остаются на старом массиве). Строка — {ptr, len} без cap, и её байты неизменяемы, поэтому у строки нет ни append, ни записи по индексу.

Глубже. Обратите внимание: «превышение ёмкости» само по себе возможно только через append (или copy в слайс достаточной длины — там ничего не растёт). Ручной выход за границы — s[i] при i >= len(s) или s[:n] при n > cap(s) — это не рост, а паника index out of range / slice bounds out of range. Ещё одно отличие от строки: конкатенация строк всегда создаёт новую строку (растить нечего), поэтому в цикле нужен strings.Builder, который внутри как раз держит []byte и пользуется амортизированным ростом.

Коротко. Массив — фиксированная длина в типе, копируется целиком, сравним, может быть ключом мапы. Слайс — заголовок ptr/len/cap, длина динамическая, копируется без данных, сравнивать можно только с nil.

Глубже. См. первый вопрос конспекта. Дополнительно к сказанному: у массива нельзя взять «подмассив» — arr[1:3] даёт именно слайс, а не массив.

Коротко. c := append(a, b...) — но осторожно, это может писать в backing array a. Безопасный вариант: c := slices.Concat(a, b) (Go 1.22+) или явная аллокация с copy.

Глубже. Идиома append(a[:len(a):len(a)], b...) заставляет append всегда аллоцировать новый массив (cap искусственно приравнен к len) — это лучший способ «объединить, не трогая a», если slices.Concat недоступен. Полностью явный вариант:

c := make([]T, 0, len(a)+len(b))
c = append(c, a...)
c = append(c, b...)

Для строк аналог — strings.Join/strings.Builder, для байтов — bytes.Join или slices.Concat.

Коротко. Реслайсингом s[:n], предварительно проверив n <= len(s) (или взяв n = min(n, len(s))).

Глубже. Два подвоха. Первый: s[:n] не копирует, поэтому результат держит весь исходный массив живым — если исходник большой, а нужен маленький префикс надолго, делайте slices.Clone(s[:n]). Второй: у результата cap остаётся прежним, и чужой append в него затрёт хвост оригинала; чтобы этого избежать — s[:n:n]. Для «последних n» симметрично s[len(s)-n:].

Коротко. Три слова: указатель на первый элемент, длина, ёмкость. 24 байта на amd64/arm64.

Глубже. См. подробный разбор runtime.slice выше. Для устного ответа полезно сразу привязать поля к операциям: len контролирует индексацию и range, cap — момент реаллокации в append, ptr — идентичность backing array.

Коротко. См. выше. Одной фразой: «пишет в свободную ёмкость, а если её нет — аллоцирует новый массив примерно вдвое больше, копирует и возвращает новый заголовок».

Глубже. Здесь уместно добавить сложность: амортизированно O(1) на элемент, худший случай одной операции O(n), и append(s, other...) копирует len(other) элементов через memmove, что быстрее поэлементного цикла.

Как бы ты исправил представленную программу, чтобы foo() не влияла на arr?

Заголовок раздела «Как бы ты исправил представленную программу, чтобы foo() не влияла на arr?»

Коротко. Передавать в foo копию: foo(slices.Clone(arr)) — или делать копию внутри функции. Альтернативы: передавать массив (он копируется сам) либо отдавать arr[:len(arr):len(arr)], если проблема именно в append, затирающем хвост.

Глубже. Сначала надо диагностировать, какое именно влияние мешает. Если foo пишет s[i] = ... — спасёт только копия данных. Если foo делает append и портит соседей — достаточно ограничить cap через full slice expression. Если foo сохраняет слайс где-то у себя — нужна копия и договорённость о владении. Самый явный и читаемый вариант:

func foo(s []int) {
s = slices.Clone(s) // работаем со своей копией
s[0] = 42
}

Но правильнее сделать копию на стороне вызывающего, чтобы контракт функции оставался честным.

Что такое слайс? Сколько памяти занимает слайс при передаче?

Заголовок раздела «Что такое слайс? Сколько памяти занимает слайс при передаче?»

Коротко. Слайс — заголовок ptr/len/cap; при передаче копируется ровно этот заголовок: 24 байта на 64-битной платформе (12 байт на 32-битной), независимо от количества элементов.

Глубже. Проверяется через unsafe.Sizeof(s) — он вернёт 24 для любого []T. Для сравнения: строка — 16 байт, интерфейс — 16 байт, мапа и канал — 8 байт (это просто указатели на hmap/hchan). Практический вывод: не надо «оптимизировать» передачу слайса через *[]T ради экономии — 24 байта копируются в регистры, а лишний уровень косвенности только мешает escape-анализу.

Коротко. Заголовок со всеми нулями: ptr == nil, len == 0, cap == 0. С ним можно делать почти всё: len, cap, range, append — работает; паникует только индексация.

Глубже. Нулевое значение слайса полезно: var s []int; s = append(s, 1) — идиоматичный старт без make. Отличие от пустого не-nil слайса ([]int{} или make([]int,0)) видно в трёх местах: s == nil даёт true/false, encoding/json сериализует nil-слайс в null, а пустой — в [], и reflect.DeepEqual(nil-slice, empty-slice) возвращает false (а вот slices.Equal — true, он сравнивает только длину и элементы). В API лучше не завязываться на это различие: проверяйте len(s) == 0.

Коротко. Строка — неизменяемая последовательность байт {ptr, len} в UTF-8; массив — фиксированный по длине набор элементов, копируемый целиком; слайс — изменяемое окно {ptr, len, cap} над массивом.

Глубже. Связи между ними: []byte(s) и string(b) копируют данные (компилятор умеет избегать копии в частных случаях вроде m[string(b)] или for i, r := range string(b)); []rune(s) декодирует UTF-8 и всегда аллоцирует; arr[:] даёт слайс поверх массива без копирования. Общая память: строка 16 байт, слайс 24 байта, массив [N]T — ровно N*sizeof(T).

Коротко. Индексация, реслайсинг s[a:b] и s[a:b:c], len/cap, append, copy, clear, range, конверсии в/из массива и строки; плюс весь пакет slices (Go 1.21+).

Глубже. Из slices регулярно нужны: Clone, Equal, Contains, Index, Sort/SortFunc/SortStableFunc, BinarySearch, Reverse, Compact, Delete, Insert, Max/Min, Concat (1.22), Grow/Clip, а также slices.Sorted(maps.Keys(m)) в связке с итераторами Go 1.23. Чего сделать нельзя: сравнить два слайса через ==, использовать слайс как ключ мапы, уменьшить cap реслайсингом (только slices.Clip или копирование), удалить элемент «бесплатно» — slices.Delete сдвигает хвост за O(n) и зануляет освободившийся конец.

Коротко. Нет. Одновременные чтение и запись одного слайса из разных горутин — data race, включая случай, когда одна горутина делает append, а другая читает.

Глубже. Тонкость: даже параллельная запись в разные индексы одного слайса безопасна с точки зрения модели памяти Go (это разные ячейки, race detector её не поймает), но параллельные append — нет, потому что они читают и пишут общий заголовок и могут реаллоцировать массив; результат — потерянные элементы или запись в освобождённый массив. Классический паттерн — собирать результаты в s[i] по заранее выделенному слайсу с make([]T, n) и индексом воркера, а append из нескольких горутин закрывать мьютексом или заменять на канал. Ложное разделение (false sharing) при записи в соседние индексы не является ошибкой корректности, но бьёт по производительности.

Если создать слайс a , присвоить его переменной b , а затем изменить элемент в исходном слайсе a - изменится ли b ? Почему это происходит и как сделать так, чтобы изменения в одном не затрагивали другой?

Заголовок раздела «Если создать слайс a , присвоить его переменной b , а затем изменить элемент в исходном слайсе a - изменится ли b ? Почему это происходит и как сделать так, чтобы изменения в одном не затрагивали другой?»

Коротко. Да, изменится: b := a копирует только заголовок, backing array остаётся общим. Чтобы разорвать связь — скопировать данные: b := slices.Clone(a) или b := make([]T, len(a)); copy(b, a).

Глубже. Важная асимметрия: изменение элемента видно обоим, а a = append(a, x) после превышения cap отвяжет a от b — и дальнейшие изменения a уже не будут видны в b. Поэтому «связаны или нет» — это состояние, которое может измениться в любой момент, и опираться на него в логике нельзя. Отдельно: slices.Clone делает shallow copy, то есть для [][]int или []*T внутренние слайсы/объекты останутся общими — глубокое копирование пишется руками.

Как устроены строки в Go на техническом уровне? Мы говорим, что строка - это слайс байтов, но при этом работаем с символами. Как это устроено внутри?

Заголовок раздела «Как устроены строки в Go на техническом уровне? Мы говорим, что строка - это слайс байтов, но при этом работаем с символами. Как это устроено внутри?»

Коротко. Строка — это {ptr *byte, len int} с неизменяемыми байтами в кодировке UTF-8. «Символов» как отдельной сущности в памяти нет: s[i] возвращает байт, а «символы» появляются только при декодировании — в for range или при конверсии в []rune.

Глубже. UTF-8 переменной длины: ASCII — 1 байт, кириллица — 2, большинство CJK — 3, эмодзи — 4. Отсюда: len(s) — это длина в байтах, а не в символах (число рун даёт utf8.RuneCountInString); s[0] для строки «Привет» вернёт 0xD0, а не ‘П’; резать строку по произвольному индексу опасно — получите битый UTF-8. for i, r := range s идёт по руническим границам, i — байтовый offset, r — руна (rune = алиас int32, кодовая точка); при некорректном байте выдаётся utf8.RuneError (U+FFFD) с шагом 1. Ещё уровень выше — графемные кластеры (é как e + U+0301, флаги, эмодзи с модификаторами): одна видимая «буква» может быть несколькими рунами, и стандартная библиотека их не разбирает — нужен golang.org/x/text. Иммутабельность позволяет строкам безопасно шариться: s[2:5] не копирует и разделяет память с оригиналом.

Почему добавление в слайс константное? Чем это гарантируется? Константно ли оно всегда?

Заголовок раздела «Почему добавление в слайс константное? Чем это гарантируется? Константно ли оно всегда?»

Коротко. Оно амортизированно константное, а не константное всегда. Гарантия — геометрический рост ёмкости: реаллокации происходят экспоненциально реже, поэтому суммарная стоимость n добавлений — O(n), то есть O(1) на элемент. Отдельный append, попавший на реаллокацию, стоит O(n).

Глубже. Формально: при росте в k раз (k > 1) суммарный объём копирований для n элементов равен n·k/(k−1) — константа, не зависящая от n. Если бы рост был аддитивным (например, +1024 элемента), суммарная стоимость стала бы O(n²). Практические оговорки: (1) отдельный append может занять миллисекунды на большом слайсе и вызвать всплеск памяти (одновременно живут старый и новый массив); (2) добавление k элементов сразу — O(k), а не O(1); (3) если элементы содержат указатели, GC приходится сканировать новый массив; (4) при известном размере лучше make([]T, 0, n) или slices.Grow — это убирает и копирования, и мусор.

Коротко. Это про map[K]V — хеш-таблицу Go. К слайсам относится только тем, что это второй базовый «ссылочный» тип: переменная типа map — это указатель на runtime.hmap, копируется дёшево, данные общие.

Глубже. Ключевые отличия от слайса: порядок обхода намеренно рандомизирован; нельзя взять адрес элемента (&m[k] не компилируется), потому что элемент может переехать при росте; запись в nil-мапу паникует, а чтение — возвращает нулевое значение; мапа не потокобезопасна (при обнаружении конкурентной записи runtime намеренно роняет процесс с concurrent map writes). В Go 1.24 реализация переведена на Swiss Tables — изменились внутренние структуры и производительность, но семантика прежняя. Подробности — в конспекте по мапам.

Коротко. «Срез» — русскоязычный термин для slice; всё сказанное выше про слайсы относится и к нему.

Глубже. Пункт из списка тем в отчёте о собеседовании, а не самостоятельный вопрос. Если на собеседовании звучит просто «срезы» — отвечайте по структуре: внутреннее устройство → append и рост → алиасинг и владение → типичные утечки → пакет slices.

Коротко. Либо fmt.Println(s)fmt умеет форматировать слайсы как [1 2 3], либо цикл for _, v := range s { fmt.Println(v) }. Это вопрос теста с вариантами ответа; правильный вариант из списка ниже — цикл с range.

Глубже. Полезные форматы: %v — значения, %+v — с именами полей у структур, %#v — Go-синтаксис ([]int{1, 2, 3}), %q — для []string. Для []byte %s печатает как строку, %x — как hex. Один нюанс: fmt.Println(s...) не скомпилируется для []int — spread в вариадик ...any работает только для []any.

Коротко. Неверный вариант: у слайса нет методов, и типа Slice в стандартной библиотеке нет. Метод можно объявить только на именованном типе (type IntSlice []int), и то он не появится сам по себе.

Коротко. Правильный вариант. range по слайсу даёт пару (индекс, копия элемента), второе значение можно игнорировать через _.

Глубже. Напомню про Go 1.22: переменные i и v теперь создаются заново на каждой итерации, поэтому взятие &v или захват v замыканием больше не приводят к классическому багу «все горутины видят последний элемент». Число итераций фиксируется в начале цикла, так что append внутри range по этому же слайсу не приведёт к бесконечному циклу.

Коротко. Неверный вариант: такого метода нет. Range есть у sync.Map (m.Range(func(k, v any) bool)), отсюда и путаница, но у слайса методов нет вовсе.

Коротко. Неверный вариант: встроенной функции echo в Go не существует.

Коротко. Неверный вариант дважды: встроенный print не вариадичен в смысле spread — print(slice...) не компилируется, и сам print/println не предназначен для прикладного кода (пишет в stderr, формат не гарантирован спецификацией, может быть удалён).

Глубже. print и println — отладочные встроенные функции без гарантий: они не форматируют структуры и слайсы так, как fmt, не проходят через io.Writer и игнорируют интерфейс Stringer. В продакшн-коде всегда fmt или log/log/slog.

Коротко. Это объявление массива из трёх int, а не слайса. В тестах на «как получить неизменяемую (в смысле копирования по значению) последовательность» это правильный вариант: массив копируется при передаче, так что вызываемая функция не изменит оригинал.

Глубже. Отличие от s := []int{1, 2, 3} — ровно в трёх точках [3] vs []: тип, семантика копирования и сравнимость. [...]int{1, 2, 3} эквивалентно первому: длину посчитает компилятор. Настоящей иммутабельности массив всё же не даёт: внутри своей области видимости s[0] = 9 разрешено; он лишь не разделяет память при копировании.

Коротко. Неверный вариант: ни freeze, ни Dint в Go не существуют — это выдуманные имена-дистракторы.

Коротко. Неверный вариант: пакета immutable в стандартной библиотеке нет. Иммутабельных коллекций в Go из коробки нет вообще; ближайшее — массив (копия по значению), строка (неизменяемые байты) и iter.Seq (Go 1.23), отдающий значения без доступа к хранилищу.

Проходя по срезам и сравнивая каждый элемент;

Заголовок раздела «Проходя по срезам и сравнивая каждый элемент;»

Коротко. Правильный вариант ответа на вопрос «как сравнить два слайса»: только поэлементно. В современном коде это slices.Equal(a, b) (Go 1.21+), а для произвольного компаратора — slices.EqualFunc.

Глубже. slices.Equal сначала сравнивает длины, потом элементы (требует comparable). Для вложенных структур и мапы внутри подойдёт reflect.DeepEqual, но он на порядок медленнее и по-разному трактует nil-слайс и пустой слайс. Для []byte есть быстрый bytes.Equal, использующий ассемблерное сравнение блоками.

Коротко. Неверный вариант: слайсы не сравнимы через ==, компилятор выдаст ошибку invalid operation: slice can only be compared to nil. Сравнить со слайсом можно только nil.

Глубже. Причина принципиальная: непонятно, какую семантику выбрать — идентичность backing array или поэлементное равенство; и второе было бы O(n) операцией, спрятанной за оператором, который во всём остальном языке O(1). По той же причине слайс не comparable, не может быть ключом мапы и не подходит под ограничение comparable в дженериках (нужен slices.Equal или свой компаратор). Массивы, в отличие от слайсов, сравнимы: [3]int{1,2,3} == [3]int{1,2,3} — true.

Коротко. Корректное объявление пустого (но не nil) слайса: len == 0, cap == 0, ptr указывает на runtime.zerobase, поэтому slice != nil.

Глубже. В большинстве случаев идиоматичнее var slice []int — nil-слайс ведёт себя так же во всех операциях, но не требует ничего от аллокатора и корректнее сериализуется в проектах, где различают null и []… хотя как раз для JSON часто нужен противоположный выбор: []int{} даёт [], nil даёт null. Linters (gosimple/staticcheck S1021 и подобные) обычно советуют var s []T, кроме случаев, когда важна не-nil-ность.

Коротко. Синтаксически корректно, но имя вводит в заблуждение: []int{} — это слайс, а не массив. Массив был бы [0]int{} или [3]int{}.

Глубже. В тестах этот вариант обычно является дистрактором к вопросу «как объявить массив». Отличие одно и оно в скобках: пустые [] — слайс, число или ... внутри — массив.

Коротко. Не компилируется: []int — это тип, а не значение. Нужно либо var arr []int, либо arr := []int{}, либо arr := make([]int, 0).

Глубже. Короткое объявление := требует выражения справа. Типичная ошибка новичка при переходе с языков, где T x и x := T внешне похожи. Формально ошибка компиляции звучит как type []int is not an expression.

функция получает слайс байт супербольшой - надо вернуть только первые 10 байт

Заголовок раздела «функция получает слайс байт супербольшой - надо вернуть только первые 10 байт»

Коротко. Нельзя возвращать b[:10] — этот подслайс держит весь огромный массив живым и не даст GC его освободить. Нужно копировать: bytes.Clone(b[:10]).

Глубже. Полная версия с защитой от коротких входов:

func head10(b []byte) []byte {
if len(b) > 10 {
b = b[:10]
}
return slices.Clone(b) // или bytes.Clone(b)
}

Это тот самый пункт «A note on memory» из блога Go про слайсы: GC освобождает объект целиком, поэтому один живой указатель в его начало (а b[:10] указывает именно туда) удерживает все 100 МБ. Если функция вызывается часто, а результат короткоживущий, можно принимать буфер от вызывающего (func head(dst, src []byte) []byte), чтобы вообще не аллоцировать.

Написать функцию, проверяющую является ли слайс монотонным? Монотонная функция - функция одной переменной, определенная на некотором подмножестве действительных чисел, которая либо везде (на области своего определения) не убывает, либо везде не возрастает.

Заголовок раздела «Написать функцию, проверяющую является ли слайс монотонным? Монотонная функция - функция одной переменной, определенная на некотором подмножестве действительных чисел, которая либо везде (на области своего определения) не убывает, либо везде не возрастает.»

Коротко. Один проход, два флага: «пока не убывает» и «пока не возрастает». Если оба сброшены — не монотонна. Пустой слайс и слайс из одного элемента монотонны по определению.

Глубже.

func IsMonotonic[T cmp.Ordered](s []T) bool {
nonDecr, nonIncr := true, true
for i := 1; i < len(s); i++ {
if s[i] < s[i-1] {
nonDecr = false
}
if s[i] > s[i-1] {
nonIncr = false
}
if !nonDecr && !nonIncr {
return false // ранний выход
}
}
return true
}

O(n) времени, O(1) памяти, ранний выход. Уточняющие вопросы, которые стоит задать самому: считается ли монотонной последовательность с равными соседями (в нестрогой трактовке — да, как в определении из условия); нужна ли строгая монотонность (тогда <=/>= вместо </>); что делать с NaN для float (cmp.Ordered включает float, а NaN ломает все сравнения — стоит проговорить). Готовая альтернатива: slices.IsSorted(s) || slices.IsSortedFunc(s, func(a, b T) int { return cmp.Compare(b, a) }), но она делает два прохода.

Дан слайс целых чисел. Напишите функцию remove, удаляющую все нули

Заголовок раздела «Дан слайс целых чисел. Напишите функцию remove, удаляющую все нули»

Коротко. Классический filter in-place: пишем в s[:0], сохраняя ненулевые элементы, и возвращаем результат. O(n) времени, ноль аллокаций.

Глубже.

func remove(s []int) []int {
out := s[:0] // переиспользуем тот же массив
for _, v := range s {
if v != 0 {
out = append(out, v)
}
}
clear(s[len(out):]) // обнуляем хвост: важно для слайсов указателей
return out
}

Что стоит проговорить: функция мутирует входной слайс — это надо документировать, а если нельзя, писать в новый make([]int, 0, len(s)). clear (встроенный, Go 1.21) нужен, когда элементы содержат указатели, иначе хвост массива удерживает объекты от сборки. Готовые аналоги в стандартной библиотеке: slices.DeleteFunc(s, func(v int) bool { return v == 0 }) — делает ровно это, включая зануление хвоста.

Требуется реализовать функцию zip, которая соединяет элементы двух слайсов в слайс пар

Заголовок раздела «Требуется реализовать функцию zip, которая соединяет элементы двух слайсов в слайс пар»

Коротко. Дженерик-функция, возвращающая []Pair[A, B], длина результата — минимум из длин входов (или ошибка при несовпадении — надо уточнить требование).

Глубже.

type Pair[A, B any] struct {
First A
Second B
}
func Zip[A, B any](a []A, b []B) []Pair[A, B] {
n := min(len(a), len(b)) // min — встроенная функция с Go 1.21
out := make([]Pair[A, B], 0, n)
for i := 0; i < n; i++ {
out = append(out, Pair[A, B]{a[i], b[i]})
}
return out
}

Обсудить с интервьюером: усечение по минимуму vs. ошибка vs. дополнение нулевыми значениями; надо ли поддержать nil входы (здесь поддержаны — вернётся пустой слайс); нужен ли ленивый вариант — в Go 1.23 естественно вернуть iter.Seq2[A, B] вместо материализованного слайса, чтобы не аллоцировать.

Реализовать версию функции zip которая сможет соединять произвольное количество слайсов

Заголовок раздела «Реализовать версию функции zip которая сможет соединять произвольное количество слайсов»

Коротко. Вариадическая версия принимает ...[]T (все слайсы одного типа — иначе типизировать результат нечем) и возвращает [][]T, где i-я строка — срез i-х элементов.

Глубже.

func ZipN[T any](ss ...[]T) [][]T {
if len(ss) == 0 {
return nil
}
n := len(ss[0])
for _, s := range ss[1:] {
n = min(n, len(s))
}
out := make([][]T, n)
for i := 0; i < n; i++ {
row := make([]T, len(ss))
for j, s := range ss {
row[j] = s[i]
}
out[i] = row
}
return out
}

Ключевое ограничение системы типов Go: нельзя записать вариадический дженерик с разными типами параметров (нет variadic type parameters), поэтому zip для разнородных слайсов делается либо через []any, либо генерацией кода на нужную арность (Zip2, Zip3, …). Оптимизация: вместо n отдельных make([]T, len(ss)) можно выделить один плоский буфер make([]T, n*len(ss)) и нарезать его — одна аллокация вместо n.

Требуется реализовать функцию uniqRandn, которая генерирует слайс длины n уникальных, рандомных чисел.

Заголовок раздела «Требуется реализовать функцию uniqRandn, которая генерирует слайс длины n уникальных, рандомных чисел.»

Коротко. Если диапазон невелик — rand.Perm(maxVal)[:n]: гарантированно уникально и за O(maxVal). Если диапазон огромен относительно n — генерировать и отсеивать дубликаты через map[int]struct{}.

Глубже.

func uniqRandn(n, maxVal int) ([]int, error) {
if n > maxVal {
return nil, fmt.Errorf("n=%d > maxVal=%d: уникальных чисел не хватит", n, maxVal)
}
seen := make(map[int]struct{}, n)
out := make([]int, 0, n)
for len(out) < n {
v := rand.IntN(maxVal) // math/rand/v2
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
out = append(out, v)
}
return out, nil
}

Что оценивает интервьюер: (1) проверили ли вы n > maxVal — иначе бесконечный цикл; (2) понимаете ли вы, что при n, близком к maxVal, метод отсева деградирует (ожидаемое число итераций растёт как гармонический ряд, ~maxVal·ln(maxVal/(maxVal−n))), и тогда правильный ответ — частичный Fisher–Yates / rand.Perm; (3) знаете ли вы про math/rand/v2 (Go 1.22) и что для криптографии нужен crypto/rand, а не math/rand; (4) что глобальный math/rand потокобезопасен, но собственный *rand.Rand — нет.

Доп вопрос: какие типы данных в Go ведут себя так же как слайс (у которого под капотом ссылка на массив?)

Заголовок раздела «Доп вопрос: какие типы данных в Go ведут себя так же как слайс (у которого под капотом ссылка на массив?)»

Коротко. Типы со ссылочной семантикой: map, chan, func, а также указатели и интерфейсы (в интерфейсе лежит указатель на данные). Строка похожа устройством (указатель на байты), но её данные неизменяемы, поэтому «эффекта общего состояния» не даёт.

Глубже. Уточнение по устройству: переменная map[K]V — это указатель на runtime.hmap (8 байт), chan T — указатель на runtime.hchan (8 байт), func — указатель на closure-объект. Все они копируются как одно слово, а данные общие, поэтому копия мапы, переданная в функцию, «видит» вставки. Слайс отличается тем, что это не один указатель, а трёхсловный дескриптор: поэтому мапа/канал ведут себя как ссылка полностью, а слайс — «наполовину» (элементы общие, len/cap — нет). Массивы, структуры и все числовые типы — чистые value-типы.

Еще доп вопрос: что будет, если заменить слайс на массив в примере выше?

Заголовок раздела «Еще доп вопрос: что будет, если заменить слайс на массив в примере выше?»

Коротко. Массив копируется по значению, поэтому функция получит независимую копию: изменения внутри функции не будут видны снаружи, а вся «магия» с общим backing array и append исчезнет.

Глубже. Кроме семантики поменяется и тип: длина попадёт в сигнатуру (func f(a [3]int)), и функция перестанет принимать наборы другой длины — это главная причина, почему массивы редко используются в API. Плюс копирование стоит N*sizeof(T) байт на каждый вызов, что для больших N заметно. Если нужен именно массив, но без копии — передают *[3]T; тогда поведение снова становится «ссылочным». Отдельно: массив нельзя append, поэтому любые примеры с ростом просто не скомпилируются.

Коротко. Пометка из отчёта: базовый блок по встроенным типам. См. выше «Стандартные вопросы про Go».

Глубже. Минимальный чек-лист для этого блока: строка (2 слова, UTF-8, immutable, руны, Builder), слайс (3 слова, append/рост, алиасинг, утечки, slices), мапа (hmap/Swiss Tables, рандомный обход, нет адреса элемента, не потокобезопасна, nil-мапа), плюс переход к сравнению: что копируется по значению, что сравнимо через ==, что может быть ключом мапы.

Если string перевести в слайс байтов и поменять его, то как отразится на базовом string?

Заголовок раздела «Если string перевести в слайс байтов и поменять его, то как отразится на базовом string?»

Коротко. Никак: []byte(s) создаёт копию байтов, поэтому изменение слайса на строку не влияет. Так и должно быть — строки неизменяемы по спецификации.

Глубже. Компилятор умеет избегать копии в нескольких безопасных случаях, когда доказано, что []byte не переживёт выражение и не будет изменён: m[string(b)], for i, r := range string(b), сравнения и конкатенации в одном выражении. Но это оптимизации, невидимые в семантике. Обойти иммутабельность можно через unsafe (unsafe.Slice(unsafe.StringData(s), len(s))) — и это UB в прямом смысле: строковые литералы лежат в read-only памяти, попытка записи даёт SIGSEGV, а если строка динамическая, вы сломаете инварианты рантайма (например, уже вычисленные хеши в мапе). Легальный zero-copy в обратную сторону — unsafe.String(&b[0], len(b)), и то с обязательством больше не менять b.

Что такое слайс? Как связан с массивом? Как работает аппенд?

Заголовок раздела «Что такое слайс? Как связан с массивом? Как работает аппенд?»

Коротко. Сводный вопрос: слайс — заголовок ptr/len/cap над массивом; связь — окно без копирования (arr[:], s[a:b:c]); append пишет в пределах cap, иначе аллоцирует новый массив и копирует.

Глубже. См. развёрнутые ответы выше. На собеседовании такой вопрос удобно отвечать одним трёхтактным блоком: «структура из трёх слов → окно над массивом, копия делит данные → append либо в существующий cap, либо новый массив с геометрическим ростом, поэтому результат всегда присваиваем».

Будут ли равны указатели у слайсов до и после аппенда?

Заголовок раздела «Будут ли равны указатели у слайсов до и после аппенда?»

Коротко. Зависит от того, хватило ли ёмкости: если len < cap, указатель тот же; если произошёл рост — новый массив и новый указатель. Гарантий нет ни в ту, ни в другую сторону.

Глубже. Проверить можно так:

before := unsafe.SliceData(s) // Go 1.20+; или &s[0], если len > 0
s = append(s, x)
after := unsafe.SliceData(s)
fmt.Println(before == after) // true, только если роста не было

Побочные эффекты смены указателя: ранее взятые &s[i] продолжают указывать на старый массив и больше не отражают изменения; подслайсы «отклеиваются»; старый массив остаётся живым, пока на него кто-то ссылается. Ещё пара тонкостей: append(s) без аргументов возвращает тот же слайс без аллокации; при len == 0 брать &s[0] нельзя (паника), поэтому unsafe.SliceData предпочтительнее — он корректно возвращает nil для nil-слайса.

Почему слайс при аппенде расширяется быстро, а мапа медленно?

Заголовок раздела «Почему слайс при аппенде расширяется быстро, а мапа медленно?»

Коротко. Рост слайса — это одна аллокация плюс memmove непрерывного блока, элементы не меняют относительных позиций. Рост мапы требует перехеширования: каждый ключ надо заново посчитать и разложить по новым бакетам, с случайным доступом к памяти.

Глубже. Разница в природе структуры: у слайса позиция элемента — это индекс, и он не зависит от размера массива, поэтому копирование сводится к одному линейному memmove (сотни ГБ/с, идеальный prefetch). У мапы позиция определяется hash mod размер, и при удвоении таблицы половина ключей должна переехать — это чтение и запись вразнобой, промахи кэша, плюс работа с tophash/контрольными байтами. Поэтому в Go рост мапы сделан инкрементальным: старая и новая таблицы сосуществуют, а эвакуация бакетов размазана по последующим операциям записи — амортизация есть, но константа заметно хуже. В Go 1.24 мапы переехали на Swiss Tables, что улучшило и локальность, и стоимость роста, но принципиальная асимметрия со слайсом сохраняется. Отсюда практический совет одинаков для обоих: задавайте начальную ёмкость (make([]T, 0, n), make(map[K]V, n)), если размер известен.

Расскажите о потенциальных сложностях при использовании встроенной функции append для слайсов.

Заголовок раздела «Расскажите о потенциальных сложностях при использовании встроенной функции append для слайсов.»

Коротко. Основные грабли: append может незаметно писать в чужой backing array (алиасинг), а может незаметно перестать это делать (реаллокация); результат обязательно нужно присваивать; при неизвестном размере — лишние аллокации и копирования.

Глубже. Развёрнутый список: (1) append(s, x) без присваивания — потерянный результат, go vet это ловит; (2) append в подслайс затирает элементы родителя, пока хватает cap; (3) b := append(a[:i], a[i+1:]...) мутирует a — типичный сюрприз при «удалении элемента»; (4) возврат наружу слайса с запасом cap позволяет вызывающему затереть ваш хвост — лечится s[:len(s):len(s)] или slices.Clip; (5) конкурентные append — гонка; (6) в цикле без предвыделенной ёмкости — O(log n) аллокаций и до 2x пикового потребления памяти; (7) удерживание указателей &s[i] через границу append; (8) append в слайсе структур с указателями создаёт новый массив, который GC придётся сканировать; (9) при использовании слайса как стека s = s[:len(s)-1] не зануляет удалённый элемент — утечка объектов, нужен s[len(s)-1] = zero или clear.

Как бы вы нашли дубликаты в большом массиве данных, не помещающемся в память?

Заголовок раздела «Как бы вы нашли дубликаты в большом массиве данных, не помещающемся в память?»

Коротко. Двумя классическими способами: (1) хеш-шардирование — разложить записи по k файлам по hash(x) mod k так, чтобы каждый файл влезал в память, и искать дубликаты внутри каждого файла отдельно (одинаковые значения гарантированно попадут в один шард); (2) внешняя сортировка — отсортировать чанками, слить k-way merge и найти соседние равные элементы.

Глубже. Выбор зависит от ограничений. Хеш-шардирование — O(n) I/O в два прохода, лучше по времени; внешняя сортировка — O(n log n), но заодно даёт отсортированный вывод и устойчива к перекосу распределения. Если допустимы приближённые ответы, первый проход можно удешевить Bloom-фильтром или Count-Min Sketch: фильтр отсеивает заведомо уникальные записи, и в память/на диск попадают только кандидаты (Bloom даёт ложноположительные, но не ложноотрицательные, поэтому ни один дубликат не потеряется). Для оценки только количества уникальных значений хватит HyperLogLog. В реализации на Go важно упомянуть: bufio.Scanner/bufio.Writer вместо поэлементного I/O, обработку шардов в несколько горутин через errgroup, переиспользование буферов, чтобы не создавать давление на GC, и container/heap для k-way merge.

Коротко. Массив — фиксированная длина как часть типа, копирование по значению, сравним через ==, годится в ключи мапы. Слайс — трёхсловный заголовок над массивом, динамическая длина, копируется без данных, сравним только с nil, поддерживает append.

Глубже. Полный разбор — в первом вопросе конспекта. Компактная формула на собеседование: «массив — это данные, слайс — это вид на данные; массив копируется целиком, слайс — тремя словами; массив нельзя вырастить, слайс можно — но append может сменить ему backing array».

Коротко. Нет. Ни заголовок слайса, ни его backing array не защищены ничем: конкурентный append из нескольких горутин — это гонка и по заголовку, и по данным. Нужна внешняя синхронизация (sync.Mutex, канал, шардирование) либо разделение слайса на непересекающиеся куски.

Глубже. Тонкость, которую полезно проговорить: по модели памяти Go разные элементы массива — это разные ячейки памяти, поэтому одновременная запись в s[0] из одной горутины и в s[1] из другой гонкой не является и -race её не покажет. Гонка возникает, когда (а) две горутины пишут один и тот же индекс, (б) кто-то делает append — потому что запись идёт в поле len заголовка и, при переполнении, в новый массив, из-за чего часть добавленных элементов может просто потеряться, (в) одна пишет элемент, а другая читает заголовок, изменённый третьей. Ещё одна ловушка: append под мьютексом безопасен только если все горутины работают через один и тот же указатель на слайс (*[]T или поле структуры), а не через свои копии заголовка.

type SafeList struct {
mu sync.Mutex
items []int
}
func (l *SafeList) Add(v int) {
l.mu.Lock()
l.items = append(l.items, v) // пишем в поле, а не в локальную копию
l.mu.Unlock()
}

Как работает append?(задача на аппенд на словах)

Заголовок раздела «Как работает append?(задача на аппенд на словах)»

Коротко. append смотрит, помещаются ли новые элементы в текущую ёмкость. Если len+k <= cap — записывает их в тот же backing array и возвращает заголовок с len+k. Если не помещаются — выделяет новый массив большей ёмкости, копирует туда старые элементы через memmove, дописывает новые и возвращает заголовок на новую память. Результат обязательно надо присвоить.

Глубже. Классическая «задача на словах» звучит так: a := []int{1,2,3}; b := append(a[:1], 10) — чему равны a и b? Ответ: a[:1] имеет len=1, cap=3, места хватает, поэтому 10 пишется прямо в a[1], и a становится [1 10 3], а b[1 10], обе разделяют один массив. Второй типовой вариант: слайс с cap в запасе передали в функцию, там сделали append — вызывающий увидит испорченный «хвост» своего массива, хотя длина у него не изменилась. Защита — трёхиндексный слайс a[:1:1], который обрезает cap и заставляет append аллоцировать новый массив, либо явный slices.Clone.

Коротко. Это структура из трёх машинных слов: указатель на начало данных в backing array, len — число доступных элементов, cap — сколько элементов помещается от начала слайса до конца массива. Сам слайс данных не хранит, он на них ссылается.

Глубже. В рантайме это type slice struct { array unsafe.Pointer; len int; cap int } (runtime/slice.go), 24 байта на amd64/arm64. Индексирование s[i] компилируется в *(array + i*sizeof(T)) с проверкой i < len, а s[i:j:k] строит новый заголовок с array+i, len = j-i, cap = k-i, не копируя данные. Из этого следуют два практических правила: несколько слайсов могут указывать на один массив и видеть чужие изменения; и маленький слайс, вырезанный из большого массива, удерживает весь массив в живых для GC — если это важно, надо копировать через slices.Clone или copy.

Что будет если мы задали слайсу cap=2 и сделали append по одному элементу 3 раза?

Заголовок раздела «Что будет если мы задали слайсу cap=2 и сделали append по одному элементу 3 раза?»

Коротко. Первые два append пишут в исходный массив, cap остаётся 2. Третий не помещается: рантайм выделяет новый массив, копирует туда два элемента и дописывает третий. Итог для make([]int, 0, 2)len=3, cap=4.

Глубже. Проверяется тривиально:

q := make([]int, 0, 2)
for i := 0; i < 3; i++ {
q = append(q, i)
fmt.Println(len(q), cap(q)) // 1 2 / 2 2 / 3 4
}

Важная поправка на всякий случай: если исходный слайс был make([]int, 2) (то есть len=2, cap=2), то уже первый append вызовет рост, и получится len=3, cap=4. И ещё: после третьего append старый массив больше не связан с новым слайсом — если на него был другой слайс, изменения через новый в нём не отразятся.

Как изменяется емкость слайса при его расширении?

Заголовок раздела «Как изменяется емкость слайса при его расширении?»

Коротко. Начиная с Go 1.18 порог — 256 элементов: пока старая cap меньше 256, ёмкость удваивается; после — растёт примерно на 25% по формуле newcap += (newcap + 3*256) / 4. Полученное значение округляется вверх до размерного класса аллокатора, поэтому фактическая cap бывает чуть больше расчётной.

Глубже. Есть ещё две ветки: если запрошенная длина больше удвоенной старой ёмкости (append(s, много, элементов, сразу)), берётся ровно требуемая длина, а потом всё равно применяется округление до size class. Реальные числа на Go 1.24 для []int: 256 → 512, 512 → 848 (832 по формуле, округлено до 848), 1024 → 1536. До Go 1.18 порог был 1024, а не 256, поэтому старые статьи говорят «до 1024 удваивается» — на собеседовании безопаснее сказать «удваивается для небольших слайсов, затем растёт медленнее; конкретные константы — деталь реализации рантайма, менявшаяся между версиями».

Что представляет собой слайс и чем отличается от массива?

Заголовок раздела «Что представляет собой слайс и чем отличается от массива?»

Коротко. Массив — значение фиксированной длины, длина в типе, копируется целиком, сравним, может быть ключом мапы. Слайс — заголовок (указатель, len, cap) поверх массива: длина динамическая, копируется только заголовок, сравнивать можно лишь с nil, ключом мапы быть не может.

Глубже. Практические следствия: передача [1024]byte в функцию — это копия килобайта на каждый вызов, передача []byte — 24 байта; arr1 == arr2 компилируется, s1 == s2 — ошибка компиляции (нужен slices.Equal или bytes.Equal); len(arr) — константа времени компиляции, что позволяет писать const n = len(arr); массив можно объявить с автоматической длиной [...]int{1,2,3}. И массив, и слайс индексируются одинаково, а range по обоим работает по значению-копии элемента (с Go 1.22 переменная цикла создаётся заново на каждой итерации, так что брать &v в цикле теперь безопасно).

Коротко. Нет. Длина массива — часть его типа и фиксируется на этапе компиляции; append к массиву не применяется вообще. Можно сделать слайс поверх массива (arr[:]) и делать append уже к нему — но тогда при переполнении данные уедут в новый массив, а исходный останется прежним.

Глубже. Обходные варианты: завести массив с запасом и хранить рядом «логическую длину» (типичный паттерн для zero-allocation кода — var buf [64]byte; n := copy(buf[:], data)), либо просто использовать слайс. Тип [N]T с константой в дженериках тоже не спасает: N фиксировано в момент инстанцирования.

Коротко. См. выше «Как работает append?». Кратко: если cap хватает — дописывает в существующий массив и увеличивает len; если нет — выделяет больший массив, копирует старое, дописывает новое и возвращает новый заголовок.

Глубже. Отличие, которое стоит добавить, если вопрос задан повторно: append может принимать переменное число аргументов и раскрытие слайса — append(a, b...), а для []byte есть спецформа append(b, "строка"...). Ещё append(nil-слайс, x) полностью корректен: nil — валидный слайс с len=cap=0, и append просто аллоцирует под него массив.

Коротко. nil: указатель нулевой, len == 0, cap == 0. Это полноценно рабочий слайс — по нему можно итерироваться, брать len/cap, делать append и copy.

Глубже. Отличие nil-слайса от пустого ([]int{} или make([]int, 0)) — только в указателе и в двух наблюдаемых местах: s == nil даёт true/false и encoding/json маршалит nil в null, а пустой — в []. reflect.DeepEqual(nil-слайс, пустой) возвращает false, а slices.Equaltrue. В API лучше не заставлять вызывающего различать эти случаи и просто проверять len(s) == 0.

Как отсортировать массив структур по алфавиту поля name?

Заголовок раздела «Как отсортировать массив структур по алфавиту поля name?»

Коротко. С Go 1.21 — slices.SortFunc с компаратором, возвращающим int. Для массива нужно сначала взять слайс: slices.SortFunc(arr[:], ...).

Глубже.

type Person struct {
Name string
Age int
}
people := []Person{{"Игорь", 1}, {"Anna", 2}, {"bob", 3}}
slices.SortFunc(people, func(a, b Person) int {
return strings.Compare(a.Name, b.Name)
})

slices.SortFunc не стабильна (внутри pdqsort); если нужна стабильность — slices.SortStableFunc. Старый способ — sort.Slice(people, func(i, j int) bool { return people[i].Name < people[j].Name }), он работает через рефлексию и заметно медленнее. Обратите внимание: strings.Compare и < сравнивают байты UTF-8, то есть «алфавит» получается ASCII-based — все заглавные латинские буквы идут раньше строчных, а кириллица после латиницы. Если нужен человеческий алфавитный порядок, сравнивайте strings.ToLower (или strings.ToCaseFold), а для настоящей локали — golang.org/x/text/collate.

Различия слайсов и массивов. Из чего состоит слайс?

Заголовок раздела «Различия слайсов и массивов. Из чего состоит слайс?»

Коротко. См. выше «Что представляет собой слайс и чем отличается от массива». Слайс состоит ровно из трёх полей: указателя на backing array, длины и ёмкости.

Глубже. Если просят «из чего состоит» — назовите три поля и добавьте, что cap считается от начала слайса до конца массива, а не от начала массива: после s := arr[2:4] для arr длины 10 будет len=2, cap=8. Это тот факт, на котором чаще всего ловят.

Коротко. См. выше «Как изменяется емкость слайса при его расширении»: удвоение до порога 256, затем ~1.25x, плюс округление до размерного класса аллокатора. Амортизированная стоимость append — O(1).

Глубже. Дополнение про стоимость: каждый рост — это mallocgc плюс memmove всех уже накопленных элементов, поэтому N последовательных append без предаллокации дают ~2N скопированных элементов и log(N) аллокаций. Если размер известен заранее, пишите make([]T, 0, n) — это и быстрее, и не даёт «рваной» ёмкости. Если элементы содержат указатели, каждая новая аллокация ещё и добавляет работы GC.

Если не присвоить результат append к исходному слайсу, изменится ли он?

Заголовок раздела «Если не присвоить результат append к исходному слайсу, изменится ли он?»

Коротко. Длина исходного слайса не изменится никогда. А вот содержимое backing array изменится, если ёмкости хватало: append запишет элемент в ячейку за пределами len, и исходный слайс её просто не видит. Если ёмкости не хватило — исходный слайс не изменится вообще, а результат будет потерян (go vet ловит это как «result of append is never used»).

Глубже.

a := make([]int, 3, 5) // [0 0 0], cap 5
append(a, 7) // не компилируется: append требует использовать результат
b := a[:2]
_ = append(b, 7) // пишет 7 в a[2]
fmt.Println(a) // [0 0 7]

Отсюда и правило «не держите два живых слайса на один массив», и приём a[:2:2] для отсечения ёмкости. Именно поэтому канонический вид вызова — s = append(s, x).

Коротко. Напрямую нельзя: слайсы не сравнимы, а ключ мапы обязан быть comparable — это ошибка компиляции «invalid map key type». Нужно привести слайс к сравнимому представлению: строке, массиву фиксированной длины или структуре.

Глубже. Рабочие варианты:

// 1) []byte -> string: конверсия в ключе мапы не аллоцирует (оптимизация компилятора)
m := map[string]int{}
m[string(b)]++
// 2) слайс известной длины -> массив (Go 1.20+)
var key [3]int = [3]int(s) // паникует, если len(s) < 3
// 3) произвольный слайс -> строковый ключ через сериализацию
key := strings.Join(parts, "\x00") // разделитель, который не встречается в данных

Чего делать не стоит: fmt.Sprint(s) как ключ (медленно и неоднозначно для вложенных типов) и хеш без хранения оригинала (коллизии). Если нужна «мапа по содержимому слайса» с возможностью восстановить ключ, храните значение как структуру {key []T; val V}.

Как проверить наличие массива в мапе (в качестве ключа у нас сам массив, а в качестве значения что будешь использовать)?

Заголовок раздела «Как проверить наличие массива в мапе (в качестве ключа у нас сам массив, а в качестве значения что будешь использовать)?»

Коротко. Массив сравним, если сравним его элемент, поэтому он может быть ключом напрямую. Проверка — обычная comma-ok идиома. Для множества значением берут struct{} — он занимает ноль байт.

Глубже.

type Key = [3]int
set := make(map[Key]struct{})
set[Key{1, 2, 3}] = struct{}{}
if _, ok := set[Key{1, 2, 3}]; ok {
// есть
}

struct{}{} предпочтительнее bool, потому что не занимает памяти (все нулевые значения указывают на runtime.zerobase) и не даёт соблазна писать m[k] = false, что для множества семантически бессмысленно. bool уместен, если действительно нужно хранить трёхзначное состояние «нет ключа / false / true». Ещё замечание к Go 1.24: реализация мап переехала на Swiss Tables, но это чисто внутренняя оптимизация — требования к ключам и семантика comma-ok не поменялись.

Коротко. По значению — копируется заголовок из трёх слов (24 байта на 64-битной платформе). Данные не копируются, поэтому функция видит и может менять те же элементы; но изменения len/cap внутри функции наружу не видны.

Глубже. Если функция должна изменить длину так, чтобы это увидел вызывающий, есть два пути: вернуть новый слайс (идиоматично, как в append) или принимать *[]T (используется реже, например json.Unmarshal внутри). Ещё нюанс escape-анализа: передача слайса в функцию сама по себе не заставляет данные уезжать в кучу, но если параметр «убегает» (сохраняется в глобал, отправляется в канал, попадает в интерфейс), компилятор аллоцирует backing array в куче — смотрите go build -gcflags=-m.

Что такое слайс? Внутренняя структура его?

Заголовок раздела «Что такое слайс? Внутренняя структура его?»

Коротко. См. выше «Как устроен слайс в Golang». Слайс — это struct{ array unsafe.Pointer; len int; cap int } из runtime/slice.go, окно в чужой массив.

Глубже. Что стоит добавить, если спрашивают именно про внутренности: тип reflect.SliceHeader описывает ту же раскладку, но с Go 1.20 он помечен как deprecated в пользу unsafe.Slice, unsafe.SliceData и reflect.Value.UnsafePointer — они безопаснее с точки зрения GC, потому что не превращают указатель в uintptr. Строка, для сравнения, — двухсловный заголовок (ptr, len) без cap, и это удобная параллель на собеседовании.

В чем разница между new и make вызовами? А для слайсов?

Заголовок раздела «В чем разница между new и make вызовами? А для слайсов?»

Коротко. new(T) выделяет память под T, зануляет её и возвращает *T. make работает только для слайса, мапы и канала, инициализирует их внутренние структуры и возвращает само значение типа T, а не указатель.

Глубже. Для слайсов разница особенно наглядна: new([]int) даёт *[]int, указывающий на nil-слайс — backing array не выделен, len=cap=0; чтобы что-то положить, всё равно придётся сделать *p = append(*p, 1) или *p = make([]int, n). А make([]int, 3, 10) сразу выделяет массив на 10 элементов, зануляет первые 3 и возвращает готовый слайс. Отдельно: new([10]int) — вполне осмысленный вызов, он даёт *[10]int с уже готовым массивом, и p[:] превращает его в слайс с len=cap=10. Из трёх «make-типов» только слайс имеет третий аргумент; у мапы второй аргумент — подсказка размера, у канала — размер буфера.

Какое у slice zero value? какие операции над ним возможны?

Заголовок раздела «Какое у slice zero value? какие операции над ним возможны?»

Коротко. Zero value — nil. С ним корректно работают len, cap (оба 0), range (ноль итераций), append, copy (копирует 0 элементов), слайсинг s[0:0], сравнение с nil, передача в функцию и функции пакета slices. Паникует только индексация s[0] и слайсинг за границы.

Глубже. Поэтому идиоматично объявлять аккумулятор как var out []T, а не out := []T{} — код проще, а append сам сделает первую аллокацию. Исключение — JSON API, где нужно отдать [], а не null: тогда явно out := []T{} либо make([]T, 0). Ещё одна деталь: nil-слайс — это не «нулевой указатель, который упадёт»; разыменования нет, потому что все операции идут через len, и проверка границ отсекает доступ раньше.

Арифметический вопрос: в пустой слайс последовательно добавили 10 элементов. Какие у него len и cap?

Заголовок раздела «Арифметический вопрос: в пустой слайс последовательно добавили 10 элементов. Какие у него len и cap?»

Коротко. len = 10. cap для []int (и любого элемента в 8 байт) будет 16: последовательность ёмкостей 0 → 1 → 2 → 4 → 8 → 16. Формально ответ «cap ≥ 10, реализация даёт 16» — точное значение зависит от размера элемента и версии Go.

Глубже. Проверка на Go 1.24 для []int даёт пары len/cap: 1/1, 2/2, 3/4, 4/4, 5/8, …, 9/16, 10/16. А вот для []byte та же процедура даст cap = 16, но путь другой: уже первый append получит cap = 8, потому что минимальный размерный класс аллокатора — 8 байт, и запрошенный 1 байт округляется до него. Это хорошая иллюстрация того, что «удвоение» — только первое приближение, а окончательное слово за roundupsize. На собеседовании стоит сразу спросить, какой тип элемента, — это показывает понимание механики.

Коротко. Присваивание b := a копирует только заголовок — оба слайса смотрят в один массив (shallow copy). Настоящая копия данных делается через copy(dst, src) или slices.Clone(a). append копирует данные лишь как побочный эффект, когда не хватило ёмкости, — полагаться на это как на способ скопировать слайс нельзя.

Глубже. Типичная ошибка — «скопировать» слайс так: b := make([]int, 0, len(a)); b = append(b, a...) — это как раз корректно; а вот b := make([]int, len(a)); copy(b, a) тоже корректно, но b := make([]int, 0, len(a)); copy(b, a) уже нет: copy копирует min(len(dst), len(src)) = 0 элементов, потому что len(dst) равен нулю. И slices.Clone, и copy делают поверхностную копию: если элементы — указатели, слайсы или мапы, то копии будут ссылаться на те же объекты, и «глубокая» копия требует ручного обхода.

Коротко. С Go 1.20 — прямой конверсией arr := [4]int(s); она паникует, если len(s) < 4. С Go 1.17 доступна конверсия в указатель на массив: p := (*[4]int)(s) — она не копирует данные, а даёт вид на тот же backing array.

Глубже.

s := []int{1, 2, 3, 4}
arr := [4]int(s) // копия данных, Go 1.20+
p := (*[2]int)(s) // разделяет память с s, Go 1.17+
p[0] = 100 // s[0] тоже станет 100

Длина массива должна быть константой времени компиляции — «сделать массив длины len(s)» невозможно в принципе, потому что длина является частью типа. До Go 1.17 единственным вариантом был var arr [4]int; copy(arr[:], s) — он же остаётся правильным ответом, когда len(s) может быть меньше N и паника нежелательна.

Как вы отсортируете массив структур по алфавиту по полю Name?

Заголовок раздела «Как вы отсортируете массив структур по алфавиту по полю Name?»

Коротко. См. выше «Как отсортировать массив структур по алфавиту поля name»: slices.SortFunc(arr[:], func(a, b T) int { return strings.Compare(a.Name, b.Name) }).

Глубже. Отличие формулировки — здесь явно сказано «массив», поэтому не забудьте arr[:]: slices.SortFunc принимает ~[]E, массив не подойдёт. И поскольку слайс arr[:] смотрит на тот же массив, сортировка изменит исходный arr на месте — если это нежелательно, сначала slices.Clone(arr[:]).

Удалить дубликаты из массива без переаллокации - можно ли?

Заголовок раздела «Удалить дубликаты из массива без переаллокации - можно ли?»

Коротко. Да, если речь о слайсе: классический in-place приём — писать уникальные элементы в начало того же backing array через out := s[:0] и вернуть out. Новой аллокации под данные не будет, изменится только длина. Для настоящего массива [N]T длину изменить нельзя, поэтому возвращают пару «массив + число валидных элементов».

Глубже.

// Порядок сохраняется, но нужна мапа-множество — она аллоцирует.
func dedup(s []int) []int {
seen := make(map[int]struct{}, len(s))
out := s[:0]
for _, v := range s {
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
out = append(out, v)
}
clear(s[len(out):]) // Go 1.21: обнулить хвост, чтобы не держать объекты живыми
return out
}

Если разрешено менять порядок и не хочется вообще ничего аллоцировать — отсортируйте на месте (slices.Sort) и примените slices.Compact, который схлопывает соседние равные элементы за один проход и тоже работает in-place: получится O(n log n) времени и O(1) дополнительной памяти. Без сортировки и без множества остаётся только O(n²) сравнений. Отдельно проговорите нюанс: s[:0] не освобождает память — старый backing array живёт целиком, а элементы-указатели в «отрезанном» хвосте остаются достижимыми, пока их не занулить (clear); именно поэтому slices.Delete и slices.Compact с Go 1.22 сами вызывают clear на хвосте.

на вход последовательность единиц и нулей, найти возможную максимально длинную последовательность единиц которая может получиться если из массива убрать 1 ноль;

Заголовок раздела «на вход последовательность единиц и нулей, найти возможную максимально длинную последовательность единиц которая может получиться если из массива убрать 1 ноль;»

Коротко. Скользящее окно с не более чем одним нулём внутри: двигаем правую границу, при появлении второго нуля подтягиваем левую, ответ — максимум по окну числа единиц в нём. O(n) времени, O(1) памяти.

Глубже.

func maxOnesAfterDropOneZero(a []int) int {
best, zeros, left := 0, 0, 0
for right, v := range a {
if v == 0 {
zeros++
}
for zeros > 1 {
if a[left] == 0 {
zeros--
}
left++
}
if n := right - left + 1 - zeros; n > best {
best = n
}
}
return best
}

Для [1,1,0,1,1,1,0,1] вернёт 5, для [1,1,1] — 3, для [0,0,0] — 0. Обязательно уточните у интервьюера граничный случай «нулей нет вовсе»: если требуется удалить ровно один элемент (формулировка LeetCode 1493), ответ для массива из одних единиц должен быть len-1, и тогда из результата всегда вычитается единица. Второй уточняющий вопрос — можно ли модифицировать вход и нужна ли сама подпоследовательность или только её длина.

Есть массив чисел [1, 2, 3, 4, 6, 7, 9] нужно свернуть его в интервалы -> [1-4, 6-7, 9];

Заголовок раздела «Есть массив чисел [1, 2, 3, 4, 6, 7, 9] нужно свернуть его в интервалы -> [1-4, 6-7, 9];»

Коротко. Один линейный проход: держим начало текущей серии, расширяем её пока следующий элемент ровно на 1 больше предыдущего, на разрыве закрываем интервал. O(n), O(1) дополнительной памяти помимо результата.

Глубже.

func ranges(a []int) []string {
out := make([]string, 0, len(a))
for i := 0; i < len(a); {
j := i
for j+1 < len(a) && a[j+1] == a[j]+1 {
j++
}
if i == j {
out = append(out, strconv.Itoa(a[i]))
} else {
out = append(out, fmt.Sprintf("%d-%d", a[i], a[j]))
}
i = j + 1
}
return out
}

Уточняющие вопросы, которые ждёт интервьюер: отсортирован ли вход (если нет — сначала slices.Sort), возможны ли дубликаты (тогда условие продолжения серии — a[j+1]-a[j] <= 1), нужно ли схлопывать пары в x-y или пару выводить как два числа, и нет ли переполнения при a[j]+1 для math.MaxInt. Предаллокация make([]string, 0, len(a)) избавляет от перевыделений в худшем случае.

Коротко. Обрывок исходника, вопрос не восстанавливается — это строка из блока ограничений какой-то алгоритмической задачи, а не самостоятельный вопрос.

Глубже. Что с таким ограничением полезно уметь делать вслух: n ≤ 10⁴ означает, что даже O(n²) ≈ 10⁸ элементарных операций проходит по времени с натяжкой, а O(n log n) и O(n) — с большим запасом; O(n³) уже нет. Проговаривание такой оценки до написания кода — то, ради чего ограничения и дают.

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

Глубже. Практический смысл ограничения: раз k ≤ n, не нужно отдельно обрабатывать случай «окно длиннее массива», и задача обычно решается скользящим окном за O(n) вместо O(n·k) наивного пересчёта.

Префикс длины 4: все элементы массивов → уникальные {1, 5, 7} и {5, 1, 7} → общие {1, 5, 7}3 .

Заголовок раздела «Префикс длины 4: все элементы массивов → уникальные {1, 5, 7} и {5, 1, 7} → общие {1, 5, 7} → 3 .»

Коротко. Обрывок исходника, вопрос не восстанавливается — это разбор примера из условия задачи, а не вопрос.

Глубже. По фрагменту угадывается задача вида «для каждого префикса длины k посчитать количество значений, встречающихся в обоих массивах»: берутся уникальные элементы префикса первого и второго массива, считается размер пересечения. Решается инкрементально двумя мапами-счётчиками и одним счётчиком общих значений: при добавлении элемента в префикс проверяем, появился ли он теперь в обоих множествах, — O(n) на все префиксы вместо O(n²).

Что такое массив и слайс, и в чем они отличаются?

Заголовок раздела «Что такое массив и слайс, и в чем они отличаются?»

Коротко. См. выше «Что представляет собой слайс и чем отличается от массива». Массив — значение фиксированной длины, длина в типе, копируется целиком, сравним. Слайс — трёхсловное окно (ptr/len/cap) в массив, длина динамическая, копируется только заголовок, сравним только с nil.

Глубже. Формулировка «что такое массив» иногда подразумевает и вопрос о размещении: массив как локальная переменная живёт на стеке, пока escape-анализ не решит иначе, а backing array слайса создаётся runtime.makeslice и чаще уезжает в кучу — хотя make([]int, 4) с константным небольшим размером и без «убегания» компилятор тоже умеет разместить на стеке.

Коротко. См. выше «Как работает append?». Проверка len+k <= cap, запись в существующий массив либо рост через runtime.growslice с копированием.

Глубже. Полезное уточнение к повторной формулировке: сам append — не обычная функция, а builtin, который компилятор разворачивает inline. В сгенерированном коде это ветка «проверить cap → записать элементы → обновить len», и только медленный путь вызывает runtime.growslice. Поэтому append в горячем цикле при достаточной ёмкости — это буквально пара инструкций, а не вызов функции.

Как работает функция append под капотом? Почему, на ваш взгляд, выбрана именно такая сигнатура?

Заголовок раздела «Как работает функция append под капотом? Почему, на ваш взгляд, выбрана именно такая сигнатура?»

Коротко. Под капотом — inline-проверка ёмкости плюс медленный путь runtime.growslice (аллокация нового массива через mallocgc, memmove старых данных). Сигнатура func append(s []T, elems ...T) []T возвращает слайс потому, что заголовок передаётся по значению и при реаллокации меняется указатель: без возврата вызывающая сторона осталась бы со старым заголовком.

Глубже. Можно развернуть аргумент шире. Альтернатива «принимать *[]T и менять на месте» была бы менее удобной: пришлось бы писать append(&s, x), нельзя было бы использовать append в выражениях (return append(dst, src...)), и любой слайс-аргумент функции стал бы неявно мутабельным по длине. Возврат значения делает семантику явной и честной: «append может вернуть другой слайс — присвой его». Плюс это единообразно с остальным Go, где нет out-параметров. Variadic-часть даёт append(a, b...) без отдельной функции concat, а спецформа append(bs, "str"...) для []byte избавляет от конверсии. Обратная сторона дизайна — та самая ловушка «забыл присвоить» и алиасинг при частичной ёмкости, которые go vet ловит лишь частично. append остаётся builtin (а не обобщённой функцией из slices) в первую очередь по историческим причинам и ради inline-оптимизации.

Что такое слайсы и чем они отличаются от массивов?

Заголовок раздела «Что такое слайсы и чем они отличаются от массивов?»

Коротко. См. выше «Что представляет собой слайс и чем отличается от массива» — ответ тот же: динамическое окно ptr/len/cap против значения фиксированной длины.

Глубже. Если вопрос повторяется в интервью, добавьте одно короткое практическое различие, которое обычно не называют: у массива длина известна компилятору, поэтому проверки границ в цикле for i := range arr часто устраняются полностью, а у слайса len — рантайм-значение, и bounds check elimination работает только когда компилятор может доказать границы (типичный приём — _ = s[n-1] перед циклом).

Являются ли слайсы в Go потокобезопасными?

Заголовок раздела «Являются ли слайсы в Go потокобезопасными?»

Коротко. См. выше «Слайс потокобезопасный?». Нет — ни чтение с параллельной записью, ни конкурентный append не защищены.

Глубже. Что добавить в повторе: безопасные паттерны без мьютекса — заранее выделить make([]T, n) и позволить каждой горутине писать только «свой» индекс (разные ячейки — разные места памяти, гонки нет), либо собирать результаты в канал и складывать в слайс из одной горутины. Проверять всегда через go test -race / go run -race: детектор гонок находит именно такие ошибки, а без него баг проявляется как случайно потерянные элементы.

Коротко. См. выше «Как работает append?». Добавляет элементы в конец слайса и возвращает результирующий слайс — тот же, если хватило ёмкости, или новый, если пришлось расти.

Глубже. Мелкие детали, которые уместны на этот вопрос: append(s) без элементов легален и просто возвращает s; append(s, xs...) может добавить сразу много элементов, и тогда ёмкость подбирается под требуемую длину; append никогда не уменьшает длину и не удаляет элементы — удаление делается через slices.Delete или s = append(s[:i], s[i+1:]...) (второй вариант оставляет мусор в хвосте, если элементы содержат указатели).

Как меняется емкость при расширении слайса?

Заголовок раздела «Как меняется емкость при расширении слайса?»

Коротко. См. выше «Как изменяется емкость слайса при его расширении»: до 256 — удвоение, дальше newcap += (newcap + 768) / 4 до достижения нужной длины, затем округление вверх до размерного класса.

Глубже. Отдельно стоит сказать, чего рантайм не делает: ёмкость никогда не уменьшается сама (s = s[:0] оставляет весь массив), и не существует функции «shrink» — освободить лишнюю память можно только через slices.Clip(s) (Go 1.21, обрезает cap до len без копирования) либо явное копирование в новый слайс нужного размера.

Коротко. copy(dst, src) копирует элементы из src в dst и возвращает число скопированных — min(len(dst), len(src)). Она не растит приёмник и не аллоцирует; корректно работает при перекрытии слайсов, потому что внутри это memmove.

Глубже. Полезные детали: у copy есть спецформа для строк — copy(dst []byte, src string); copy возвращает 0, если у приёмника нулевая длина (частая ошибка — make([]T, 0, n) вместо make([]T, n)); для сдвига элементов внутри одного слайса copy(s[i:], s[i+1:]) полностью корректен благодаря memmove-семантике. С Go 1.21 slices.Clone(s) — идиоматичная замена связки make+copy, а clear(s) зануляет все элементы, не меняя длины.

Коротко. См. выше — runtime.nextslicecap: если требуемая длина больше удвоенной старой ёмкости, берётся она; иначе при oldCap < 256 ёмкость удваивается, а при oldCap >= 256 в цикле newcap += (newcap + 3*256) / 4, пока не покроет нужную длину. Итог округляется вверх до размерного класса аллокатора в roundupsize.

Глубже. Именно из-за roundupsize наблюдаемые числа не совпадают с «чистой» формулой: для []int на Go 1.24 после 512 получается 848, а не 832 (832 × 8 = 6656 байт округляются до класса 6784 = 848 × 8). Смотреть это стоит прямо в исходниках: runtime/slice.go (growslice, nextslicecap) и runtime/msize.go (roundupsize, таблица size classes в runtime/sizeclasses.go). И повторюсь: конкретные константы — не часть спецификации языка, они менялись (порог был 1024 до Go 1.18), поэтому опираться на них в коде нельзя.

  • Говорят «слайс передаётся по ссылке». Он передаётся по значению — копируется 24-байтовый заголовок; ссылочным выглядит только доступ к элементам.
  • Утверждают, что «capacity всегда удваивается». С Go 1.18 удвоение только до 256 элементов, дальше коэффициент плавно съезжает к 1.25x, и результат ещё округляется до size-класса аллокатора.
  • Путают «append не влияет на исходный слайс» и «влияет»: правильный ответ — «зависит от того, хватило ли cap», и именно эта недетерминированность и есть проблема.
  • Забывают, что s[:10] от огромного слайса удерживает весь backing array и не даёт GC освободить память; нужен slices.Clone/bytes.Clone.
  • Считают, что len(s) для строки — это число символов. Это число байт; символы (руны) считает utf8.RuneCountInString.
  • Пытаются сравнить слайсы через == или использовать слайс как ключ мапы. Нужен slices.Equal, ключом может быть массив, но не слайс.
  • Смешивают nil-слайс и пустой слайс, а потом удивляются null вместо [] в JSON. В логике API проверяйте len(s) == 0, а для сериализации выбирайте явно.
  • Пишут for _, v := range bigStructs и мутируют v, ожидая изменения оригинала. Нужен индекс: s[i] или &s[i].
  • Считают параллельные append в один слайс безопасными, потому что «пишем в разные элементы». append меняет общий заголовок и может реаллоцировать массив — это гонка.
  • Говорят «слайс — это динамический массив» и на этом останавливаются, не называя три поля заголовка и не объясняя разницу между len и cap.
  • Утверждают, что cap всегда удваивается. С Go 1.18 порог 256 (раньше 1024), после него рост ~1.25x, плюс округление до размерного класса аллокатора.
  • Считают, что слайс передаётся «по ссылке». Он передаётся по значению — копируется заголовок; отсюда и необходимость s = append(...), и то, что изменение длины внутри функции наружу не видно.
  • Путают «cap от начала массива» и «cap от начала слайса»: после arr[2:4] для [10]int будет cap = 8, а не 10.
  • Не видят алиасинга: append к слайсу с запасом ёмкости молча перезаписывает элементы соседнего слайса на том же массиве. Лечится трёхиндексным слайсингом s[a:b:b] или slices.Clone.
  • Считают nil-слайс непригодным к использованию и пишут лишнее if s == nil { s = []T{} }. append, len, range и copy с nil работают штатно.
  • Уверены, что s = s[:0] или s = s[:n] освобождает память. Backing array остаётся жив целиком, а элементы-указатели в хвосте продолжают удерживать объекты, пока их не занулить через clear.
  • Пишут b := make([]T, 0, len(a)); copy(b, a) и удивляются пустому результату: copy смотрит на len(dst), а не на cap.
  • Считают конкурентную запись в разные индексы одного слайса гонкой (это не гонка) и одновременно считают конкурентный append безопасным (это гонка).

Список исходных вопросов с привязкой к компаниям: ../questions/slices-arrays.md