Слайсы, мапы и строки
Встроенные коллекции Go от основ до устройства — создание и индексирование слайсов, выражения среза, заголовок и рост среза, алиасинг, мапы с проверкой comma-ok и порядком обхода, внутреннее устройство map и nil-карта, строки как неизменяемые байты.
25 вопросов
JuniorТеорияОчень частоКак создать map и как проверить наличие ключа в map в Go?
Как создать map и как проверить наличие ключа в map в Go?
Создайте через make(map[K]V) или литералом map[K]V{...}. Запись — m[k] = v, чтение — m[k]. Простое чтение никогда не падает: отсутствующий ключ возвращает нулевое значение типа значения, поэтому «нет ключа» не отличить от «есть, но ноль». Форма comma-ok v, ok := m[k] это решает — ok равно false только когда ключа нет. Учтите: nil map читается нормально, но паникует на записи, так что сначала вызовите make.
Типичные ошибки
- ✗Писать в nil map (объявлена, но без
make) и ловить панику - ✗Принимать чтение нулевого значения за доказательство отсутствия ключа
- ✗Забывать форму comma-ok и путать отсутствие с нулём
Уточняющие вопросы
- →Почему чтение nil map не паникует, а запись в неё — да?
- →Как удалить ключ и что вернёт его чтение после этого?
JuniorТеорияОчень частоЧто именно такое slice в Go и как append его наращивает?
Что именно такое slice в Go и как append его наращивает?
Slice — это лёгкое представление поверх backing array: заголовок из указателя, длины и ёмкости (cap). append добавляет элементы в конец; пока есть свободная ёмкость, он пишет на месте, но как только длина превысила бы cap, он выделяет новый, больший backing array, копирует туда старые элементы и возвращает slice, указывающий на него. Поскольку возвращённый заголовок может отличаться, результат нужно переприсвоить: s = append(s, x).
Типичные ошибки
- ✗Звать
append(s, x)без переприсваивания, теряя выросший slice - ✗Считать, что
appendвсегда меняет исходный backing array на месте - ✗Путать длину с cap или думать, что slice копирует свои данные
Уточняющие вопросы
- →Какой коэффициент роста использует Go при перевыделении в
appendи почему? - →После перевыделения в
appendвидят ли изменение прежние slice?
JuniorТеорияОчень частоЧем массивы фиксированного размера отличаются от срезов в Go и как они передаются?
Чем массивы фиксированного размера отличаются от срезов в Go и как они передаются?
Массив (array) имеет фиксированную длину, входящую в его тип; это значение, поэтому присваивание или передача копирует каждый элемент. Срез (slice) — небольшой заголовок на backing array; при передаче копируется только заголовок, и обе копии разделяют элементы.
Типичные ошибки
- ✗Думать, что срезы передаются по ссылке — заголовок копируется по значению, но указывает на общий backing array
- ✗Считать, что длина массива не входит в тип —
[3]intи[4]int— разные, несовместимые типы - ✗Ожидать, что массив, переданный в функцию, будет изменён вызываемой функцией
Уточняющие вопросы
- →Что происходит при передаче большого массива в функцию — и как избежать копирования?
- →Могут ли два среза разделять один backing array, и каковы последствия?
JuniorКодОчень частоЧто выведет сравнение nil-среза и пустого среза?
Что выведет сравнение nil-среза и пустого среза?
nil-срез (var a []int) не имеет backing array, и a == nil равно true; пустой срез ([]int{}) ненулевой, с len 0. Оба поддерживают append и range. Для «нет результатов» предпочитают nil — но nil-срез маршалится в JSON как null, а пустой — как [].
Типичные ошибки
- ✗Считать, что в
nil-срез нельзя делатьappend—appendвыделяет backing array при первом росте - ✗Полагать, что
[]int{}равенnil— он ненулевой - ✗Упускать, что
nil-срез маршалится в JSON какnull, а не[]
Уточняющие вопросы
- →Почему
appendработает одинаково наnil-срезе и пустом, несмотря на разницу== nil? - →Когда различие JSON
nullпротив[]реально ломает контракт API?
JuniorТеорияОчень частоИз каких трёх полей состоит заголовок среза (slice header) в Go?
Из каких трёх полей состоит заголовок среза (slice header) в Go?
Заголовок среза — это три машинных слова: указатель на первый элемент backing array, длина len (число доступных элементов) и ёмкость cap (число элементов от указателя до конца backing array). Сами элементы заголовок не содержит.
Типичные ошибки
- ✗Путать
lenиcap—lenэто доступные элементы,capэто запас до реаллокации - ✗Думать, что заголовок среза хранит сами элементы, а не указатель на них
- ✗Полагать, что заголовок — это один указатель, поэтому копирование среза — это одно машинное слово
Уточняющие вопросы
- →На что указывает поле-указатель после ресрайса
s = s[2:]? - →Как связаны
lenиcapпосле выражения среза видаs[low:high:max]?
JuniorТеорияЧастоЧто возвращает индексация строки Go через s[i] и что считает len(s)?
Что возвращает индексация строки Go через s[i] и что считает len(s)?
s[i] возвращает один byte (то есть uint8) — i-й сырой UTF-8 байт, а не символ. len(s) тоже считает байты, а не руны. ASCII-символ занимает один байт, но любой многобайтовый UTF-8 символ (скажем, é или ы) растягивается на несколько байт, поэтому len завышает счёт для такого текста, а s[i] может попасть в середину символа. Чтобы работать с символами, проходите строку через range (он декодирует руны) или конвертируйте через []rune(s) и индексируйте его.
Типичные ошибки
- ✗Думать, что
s[i]даёт символ или строку из одной руны, а неbyte - ✗Считать, что
len(s)считает символы — он завышает счёт многобайтового текста - ✗Индексировать UTF-8 текст и попадать в середину символа вместо
range
Уточняющие вопросы
- →Почему
for i, r := range sдаёт другие индексы, чемs[i], на UTF-8 тексте? - →В чём разница между
byteиruneв Go?
JuniorКодЧастоЧто будет при чтении, а затем записи nil-map?
Что будет при чтении, а затем записи nil-map?
Чтение m["x"] допустимо и возвращает нулевое значение 0 — чтение nil-map никогда не паникует. Запись m["x"] = 1 паникует с assignment to entry in nil map. Перед записью map нужно инициализировать через make(map[string]int) или литерал.
Типичные ошибки
- ✗Думать, что чтение
nil-map паникует — паникует только запись - ✗Ожидать, что запись лениво выделит map, как
appendраститnil-срез - ✗Забывать, что map, объявленная как
var m map[K]V, этоnil, а не пустая map
Уточняющие вопросы
- →Почему в
nil-срез можно делатьappend, а вnil-map писать нельзя? - →Что возвращает
len(m)дляnil-map и паникует ли проходrangeпо ней?
MiddleКодЧастоЧто покажут вывод cap и строки x и y после append здесь?
Что покажут вывод cap и строки x и y после append здесь?
Выведет 4 4, затем x: [a b z d] и y: [a b z]. У y := x[:2] len 2, но он наследует cap 4 до конца backing array x, поэтому у append(y, "z") есть запас, и он пишет в общий x[2], перезаписывая c вместо аллокации.
Типичные ошибки
- ✗Думать, что
cap(y)равен 2 — reslice наследует cap до конца массива родителя, поэтому он 4 - ✗Считать, что
appendвсегда аллоцирует — при запасе cap он перезаписывает общий элемент на месте - ✗Забывать, что длина
xне меняется — лишь значениеx[2]становитсяz
Уточняющие вопросы
- →Как
y := x[:2:2]изменитcap(y)и результатappend? - →Почему длина
xостаётся 4, хотяx[2]был перезаписан?
JuniorТеорияИногдаВ каком порядке range обходит элементы map в Go?
В каком порядке range обходит элементы map в Go?
Порядок случайный — Go намеренно начинает каждый range по map со случайного бакета, поэтому порядок обхода не стабилен между запусками и даже между циклами в одном запуске. Это сделано специально, чтобы код случайно не завязывался на внутреннее устройство map. Если нужен стабильный порядок, соберите ключи в slice и отсортируйте его.
Типичные ошибки
- ✗Полагаться на стабильность порядка обхода map между запусками
- ✗Считать, что map сохраняет порядок вставки, как упорядоченный словарь
- ✗Думать, что map обходится в порядке сортировки ключей
Уточняющие вопросы
- →Почему команда Go намеренно сделала порядок обхода map случайным?
- →Как напечатать элементы map в детерминированном, отсортированном порядке?
JuniorКодИногдаЧто выведет этот сниппет со slice-выражениями для x, y, z, d и e?
Что выведет этот сниппет со slice-выражениями для x, y, z, d и e?
Выведет x: [a b c d], y: [a b], z: [b c d], d: [b c], e: [a b c d]. Форма x[low:high] берёт элементы с индекса low по high-1, поэтому её длина равна high-low; опущенная граница по умолчанию равна 0 или len(x).
Типичные ошибки
- ✗Считать границу
highвключительной —x[1:3]берёт индексы 1 и 2, а не 1, 2, 3 - ✗Забывать, что опущенный low по умолчанию равен
0, а опущенный high —len(x) - ✗Считать, что
x[:]копирует массив — он возвращает срез по тому же backing array
Уточняющие вопросы
- →Какую ёмкость (cap) имеет
y := x[:2]и почему не 2? - →Как скопировать элементы, чтобы результат не делил backing array с
x?
JuniorКодИногдаЧто делает этот фрагмент, когда индексирует и пытается присвоить s[0]?
Что делает этот фрагмент, когда индексирует и пытается присвоить s[0]?
Код не компилируется. Строка в Go — неизменяемая последовательность байтов только для чтения, поэтому s[0] = ... недопустимо. (s[0] читает byte, а не строку, так что "R" ещё и не тот тип.) Чтобы поменять байт, конвертируйте: b := []byte(s); b[0] = 'R'; s = string(b).
Типичные ошибки
- ✗Думать, что строку можно менять на месте, присваивая по индексу как срезу
- ✗Считать, что
s[0]даёт строку из одного символа, а не значениеbyte - ✗Полагать, что присваивание компилируется и падает (или ничего не делает) в рантайме, а не является ошибкой сборки
Уточняющие вопросы
- →Почему
fmt.Println(s[0])печатает число, а не буквуt? - →После
b := []byte(s)влияет ли изменениеbна исходную строкуs?
JuniorТеорияИногдаКак string представлен в памяти в Go и сколько весит значение string?
Как string представлен в памяти в Go и сколько весит значение string?
Значение string — это заголовок из двух слов: 8-байтный указатель на неизменяемые UTF-8 байты плюс 8-байтная длина, поэтому на 64-битной платформе оно занимает 16 байт независимо от длины текста. Байты лежат отдельно, поэтому структура с одним полем string тоже занимает 16 байт.
Типичные ошибки
- ✗Считать, что размер строки растёт с длиной текста, а не является фиксированным заголовком в 16 байт
- ✗Путать заголовок строки с заголовком среза, добавляя несуществующее поле ёмкости
- ✗Думать, что указатель ведёт на изменяемый буфер, в который можно писать
Уточняющие вопросы
- →Почему две строки могут разделять один backing-массив после среза вроде
s[1:3]? - →Во что обходится преобразование между
stringи[]byteпо аллокациям?
MiddleТеорияИногдаЧем nil slice и nil map различаются при чтении, append и записи?
Чем nil slice и nil map различаются при чтении, append и записи?
И nil slice, и nil map читаются безопасно — len равен 0, обход через range ничего не даёт, а чтение nil map возвращает нулевое значение типа значения. Расхождение — на записи. К nil slice можно свободно применять append: он выделяет свежий backing array, поэтому var s []int; s = append(s, 1) просто работает и не требует инициализации. А вот nil map паникует в тот же миг, как вы присваиваете m[k] = v; перед любой записью нужно вызвать make(map[K]V) (или использовать литерал).
Типичные ошибки
- ✗Считать, что nil map принимает запись так же, как nil slice принимает
append - ✗Звать
makeна slice передappend, когда это не нужно - ✗Забывать, что отличается лишь запись — чтение безопасно на обоих
Уточняющие вопросы
- →Почему
appendработает на nil slice, а присваивание не работает на nil map? - →Когда вернуть nil slice предпочтительнее, чем пустой?
MiddleКодИногдаЧто выведут три строки Println после этих append?
Что выведут три строки Println после этих append?
Все три выводят исходные значения — [1 2 3 4 5], [2 3 4 5], [3 4]. У slice1 cap равен 4, у slice2 — 3, поэтому добавление двух элементов переполняет обе ёмкости. Каждый append выделяет новый backing array и переприсваивает лишь локальную копию заголовка, поэтому заголовки вызывающей стороны и arr этого не видят.
Типичные ошибки
- ✗Считать, что
appendвсегда меняет backing array вызывающей стороны — он пишет на месте только при запасеcap - ✗Забывать, что переприсваивание в
appendзатрагивает лишь локальную копию заголовка в параметре, а не переменную вызывающей стороны - ✗Неверно считать ёмкость
slice2— уslice1[1:3]capравен 3, а не 2, поэтому дваappendвсё равно переполняют
Уточняющие вопросы
- →Если бы у
slice2capбыл 4 вместо 3, какая строкаPrintlnизменилась бы и почему? - →Как изменился бы вывод, если бы
ModifySliceвозвращала срез, а вызывающая сторона переприсваивала его?
MiddleТеорияИногдаКогда стоит выбрать массив фиксированного размера вместо слайса в Go?
Когда стоит выбрать массив фиксированного размера вместо слайса в Go?
Выбирайте массив, когда длина — фиксированная константа времени компиляции и нужна семантика значения: массивы сравнимы через ==, годятся в ключи map и копируются целиком, избегая отдельного выделения под буфер. Выбирайте слайс (по умолчанию), когда длина меняется или растёт, либо когда нужно передать представление, не копируя каждый элемент.
Типичные ошибки
- ✗Брать массивы по умолчанию для данных переменной длины вместо слайсов, потом борясь с фиксированной длиной
- ✗Забывать, что массивы сравнимы и копируются по значению, а слайсы — нет и делят хранилище
- ✗Полагать, что слайс всегда в куче, поэтому массив всегда быстрее
Уточняющие вопросы
- →Почему массив фиксированного размера может быть ключом map, а слайс — нет?
- →Чем передача большого массива по значению отличается от передачи заголовка слайса?
MiddleТеорияИногдаЧто такое buffer overflow, и может ли он случиться в обычном коде на Go?
Что такое buffer overflow, и может ли он случиться в обычном коде на Go?
Buffer overflow — это запись за пределы массива или буфера, портящая соседнюю память; возможна в C, где нет проверок границ. В обычном Go её не бывает: обращения к slice и массиву проверяются по границам, и индекс вне диапазона вызывает panic, а не затирает память. Обойти это можно только пакетом unsafe.
Типичные ошибки
- ✗Думать, что Go пропускает проверки границ ради скорости, как C
- ✗Считать, что
appendза пределы capacity переполняет память, а не выделяет новый backing-массив - ✗Забывать, что
unsafe— единственная лазейка в обход проверки границ
Уточняющие вопросы
- →Какую runtime-ошибку даёт индекс слайса вне диапазона, и как она звучит?
- →Почему компилятор иногда убирает проверки границ, и как убедиться, что он это сделал?
MiddleТеорияИногдаКак map в Go устроен внутри — через бакеты и хеширование?
Как map в Go устроен внутри — через бакеты и хеширование?
Map в Go — это структура hmap, указывающая на массив бакетов. Каждый бакет хранит до 8 слотов ключ/значение плюс ссылку на overflow-бакеты. Хеш ключа выбирает бакет; старшие биты хеша ускоряют сканирование слотов. Когда бакеты заполняются, map растёт и перехеширует данные в больший массив.
Типичные ошибки
- ✗Думать, что map — это дерево или плоский массив, а не хеш-таблица на бакетах
- ✗Считать, что коллизия хешей перезаписывает или теряет записи, а не уходит в overflow-бакеты
- ✗Полагать, что бакет хранит одну запись, а не до 8 слотов
Уточняющие вопросы
- →Почему Go намеренно рандомизирует порядок итерации по map?
- →Что такое overflow-бакет и когда runtime его выделяет?
MiddleКодИногдаЧто выведет append(a[:1], 99) для a и b?
Что выведет append(a[:1], 99) для a и b?
Выведет [1 99 3] [1 99]. У a[:1] len 1, но cap 3, поэтому у append есть запас, и он пишет 99 в тот же backing array по индексу 1, перезаписывая a[1]. Принудить копию можно полным выражением среза a[:1:1], чтобы append реаллоцировал.
Типичные ошибки
- ✗Считать, что
appendвсегда возвращает новый массив — он переиспользует backing array, когдаcap > len - ✗Путать
lenиcap— уa[:1]len 1, ноcap 3унаследован отa - ✗Забывать, что полное выражение среза
a[:1:1]ограничивает ёмкость, заставляя реаллокацию
Уточняющие вопросы
- →Как
a[:1:1]меняет ёмкость и почему это предотвращает алиасинг? - →Если два вызова
appendделят один базовый срез с запасомcap, почему второй перезаписывает первый?
MiddleТеорияИногдаКак защитить срез от разделения backing array с другим срезом?
Как защитить срез от разделения backing array с другим срезом?
Сделайте независимую копию через copy(dst, src) в свежевыделенный срез или ограничьте ёмкость трёхиндексным выражением s[low:high:max], чтобы следующий append был вынужден реаллоцировать, а не перезаписывать общие элементы. Возвращайте из API копии, а не под-срезы, чтобы вызывающий не менял ваш backing array.
Типичные ошибки
- ✗Путать передачу по указателю с копированием — указатель всё равно достаёт тот же backing array
- ✗Забывать третий индекс —
s[low:high]сохраняет ёмкость родителя, поэтомуappendвсё ещё алиасит - ✗Возвращать из API под-срез, позволяя вызывающим менять внутренний backing array
Уточняющие вопросы
- →Почему
copyтребует, чтобы у приёмника была достаточная длина, а не ёмкость? - →Чем
s[low:high:max]отличается отs[low:high]для следующегоappend?
MiddleТеорияИногдаКак растёт срез, когда append превышает его ёмкость (cap) в Go?
Как растёт срез, когда append превышает его ёмкость (cap) в Go?
Когда append требует больше места, чем cap, runtime выделяет новый, больший backing array, копирует туда существующие элементы и возвращает новый заголовок на него. Ёмкость примерно удваивается для небольших срезов и растёт с меньшим коэффициентом (около 1.25x) для больших.
Типичные ошибки
- ✗Думать, что
appendрасширяет backing array на месте, а не выделяет новый - ✗Считать, что рост всегда точно удваивает — большие срезы растут с меньшим коэффициентом
- ✗Полагать, что другие срезы, разделяющие старый backing array, видят добавленные элементы после реаллокации
Уточняющие вопросы
- →Почему результат
appendвсегда нужно присваивать обратно переменной-срезу? - →Как предварительное выделение через
make([]T, 0, n)избегает повторных реаллокаций?
MiddleТеорияИногдаКакие ловушки есть у срезов Go из-за общего backing array?
Какие ловушки есть у срезов Go из-за общего backing array?
Срезы делят backing array, отсюда три ловушки. Запись через один срез меняет другие там, где их диапазоны пересекаются. append может затронуть, а может не затронуть оригинал — в зависимости от запаса ёмкости. А маленький под-срез держит весь большой массив живым, мешая GC. Смягчают это copy или s[low:high:max].
Типичные ошибки
- ✗Считать под-срез независимой копией — он алиасит backing array родителя
- ✗Считать, что
appendвсегда реаллоцирует — в пределах запасаcapон перезаписывает общие элементы - ✗Упускать удержание памяти — крошечный под-срез может удерживать огромный массив
Уточняющие вопросы
- →Как трёхиндексный срез
s[low:high:max]предотвращает алиасинг при следующемappend? - →Почему маленький под-срез большого массива может вызвать утечку памяти и как это исправить?
MiddleКодИногдаЧто напечатает цикл после переприсваивания slice во время range?
Что напечатает цикл после переприсваивания slice во время range?
Печатает a b c d. range вычисляет операнд один раз в начале и итерируется по копии заголовка slice (указатель, len, cap). Переприсваивание lst совершенно новому slice лишь переназначает переменную — цикл продолжает идти по исходному backing-массиву. Если же изменить элемент исходного backing-массива, например lst[3] = "z", цикл это увидит и напечатает a b c z.
Типичные ошибки
- ✗Думать, что
rangeперечитывает переменную slice на каждой итерации - ✗Считать, что переприсваивание переменной меняет итерируемый backing-массив
- ✗Ожидать panic вместо стабильного обхода исходного массива
Уточняющие вопросы
- →Почему изменение
lst[3]видно в цикле, а переприсваиваниеlst— нет? - →Какие три поля копируются, когда
rangeделает снимок заголовка slice?
SeniorТеорияРедкоКак поиск в map сканирует бакет — байты tophash и цепочки overflow-бакетов?
Как поиск в map сканирует бакет — байты tophash и цепочки overflow-бакетов?
Поиск хеширует ключ, выбирает бакет, затем сравнивает старший байт хеша (tophash) с каждым из 8 слотов бакета — это 1-байтовый фильтр, пропускающий полное сравнение ключа при несовпадении. Полное сравнение ключа запускает только совпадение tophash. Если все 8 слотов не подошли, поиск идёт по указателю на overflow-бакет и повторяется по цепочке.
Типичные ошибки
- ✗Думать, что runtime делает полное сравнение ключа в каждом слоте, а не фильтрует сначала по 1-байтовому
tophash - ✗Считать, что коллизия хеша перезаписывает запись, а не уходит по цепочке в overflow-бакет
- ✗Полагать, что совпадение
tophashозначает равенство ключей, и пропускать полное сравнение ключа
Уточняющие вопросы
- →Когда runtime выделяет overflow-бакет вместо роста всей map?
- →Как байт
tophashещё кодирует маркеры пустого слота и эвакуации?
SeniorТеорияРедкоПочему нельзя взять адрес элемента map, и что такое эвакуация бакетов?
Почему нельзя взять адрес элемента map, и что такое эвакуация бакетов?
Элемент map не адресуем, потому что рост перемещает записи: превысив коэффициент загрузки, map выделяет больший массив бакетов и эвакуирует записи инкрементально — каждый бакет перехешируется и копируется при последующих записях. Сохранённый &m[k] повис бы после перемещения, поэтому он не компилируется.
Типичные ошибки
- ✗Думать, что
&m[k]не работает лишь для неэкспортируемых типов, а не для любого элемента map - ✗Считать, что эвакуация бакетов происходит вся сразу во время вызвавшей её вставки
- ✗Полагать, что указатель, полученный из map, остался бы валидным при последующих вставках
Уточняющие вопросы
- →Как изменить on-place структуру-значение в map, если элементы не адресуемы?
- →Почему runtime распределяет эвакуацию по записям, а не делает её сразу?