Слайсы, мапы и строки
Встроенные коллекции Go от основ до устройства — создание и индексирование слайсов, выражения среза, заголовок и рост среза, алиасинг, мапы с проверкой comma-ok и порядком обхода, внутреннее устройство map и nil-карта, строки как неизменяемые байты.
33 вопроса
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ТеорияОчень частоЧто выведет программа?
Что выведет программа?
1 2 3
Открыть задачу →Типичные ошибки
- ✗Ждать
9 2 3: уsнет свободной ёмкости, поэтомуappendкопирует элементы в новый массив, иt[0] = 9меняет только копию. - ✗Ждать
1 3 3или ошибку:appendне меняет самs, а заполненный срез — не ошибка, он просто переезжает в массив побольше.
Уточняющие вопросы
- →Какой коэффициент роста использует Go при перевыделении в
appendи почему? - →После перевыделения в
appendвидят ли изменение прежние срезы?
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ТеорияЧастоЧто выведет программа?
Что выведет программа?
1 9
Открыть задачу →Типичные ошибки
- ✗Ждать
9 9, будтоbделит массив сa: присваивание массива копирует все его элементы, поэтому запись вb[0]не меняетa. - ✗Ждать ошибку компиляции, как в C, где массив нельзя присвоить: в Go
b := aдопустимо и копирует весь массив.
Уточняющие вопросы
- →Как сделать, чтобы
bвидел те же элементы, что иa? - →Что происходит с массивом вызывающего, если функция получает его параметром
[3]intи меняет?
JuniorТеорияЧастоЧто выведет программа?
Что выведет программа?
[10 20 30 40] [1 5]
Открыть задачу →Типичные ошибки
- ✗Ждать
[10 1 5 40]вa: третий индекс ограничивает ёмкостьbединицей, поэтомуappendкопируетbв новый массив и не трогаетa. - ✗Ждать, что
b[0] = 1изменитa: после того какappendперенёсbв новый массив, общих элементов у срезов больше нет.
Уточняющие вопросы
- →Что напечатает программа, если написать
b := a[1:2]? - →Когда функции стоит отдавать
s[i:j:j], а неs[i:j]?
JuniorТеорияЧастоЧто выведет программа?
Что выведет программа?
4 [0 0 0 7]
Открыть задачу →Типичные ошибки
- ✗Ждать
1 [7], будтоmake([]int, 3)только резервирует место: он создаёт три элемента, равных нулю, иappendдописывает7после них. - ✗Считать, что
appendзаполнит первый ноль: нули — обычные элементы, поэтомуappendвсегда пишет после последнего и длина становится4.
Уточняющие вопросы
- →Как изменить вызов
make, чтобы программа напечатала1 [7]? - →Почему результат
appendнужно присвоить обратно вs?
JuniorТеорияЧастоЧто выведет программа?
Что выведет программа?
0 true 0 false
Открыть задачу →Типичные ошибки
- ✗Ждать, что
okдля"a"будетfalse, раз значение0:okговорит, есть ли ключ вmap, а"a"в ней есть. - ✗Ждать, что отсутствующий ключ прочитается как
-1: он читается как нулевое значение типа значения, а дляintэто0.
Уточняющие вопросы
- →Почему одно
m["b"] == 0не говорит, есть ли ключ"b"? - →Как написать
if, который выполняется, только если ключ есть вmap?
JuniorТеорияЧастоЧто произойдёт при сборке и запуске программы?
Что произойдёт при сборке и запуске программы?
Выведет 0, а затем паника
Открыть задачу →Типичные ошибки
- ✗Ждать
map[a:1], будто первая запись создастmap: объявленнаяmapравнаnil, и запись в неё вызывает паникуassignment to entry in nil map. - ✗Ждать паники на
len(m): чтение из nil-map безопасно, она ведёт себя как пустая, поэтомуlen(m)равно0, а паника будет только при записи.
Уточняющие вопросы
- →Как изменить объявление
m, чтобы запись сработала? - →Что возвращает
m["x"], еслиm—nil-map?
JuniorТеорияЧастоЧто произойдёт при сборке и запуске программы?
Что произойдёт при сборке и запуске программы?
Не скомпилируется
Открыть задачу →Типичные ошибки
- ✗Ждать
Go, будто строка — изменяемый массив байтов: строки в Go неизменяемы, поэтомуs[0] = 'G'отклоняется с ошибкойcannot assign to s[0]. - ✗Ждать паники при выполнении: компилятор уже знает, что
s— строка, поэтому ошибка останавливает сборку и программа не запускается.
Уточняющие вопросы
- →Как получить строку
"Go"изs? - →Что возвращает
s[0], если его прочитать, а не записать?
JuniorТеорияЧастоЧто выведет программа?
Что выведет программа?
209 4
Открыть задачу →Типичные ошибки
- ✗Ждать
ёили её код 1105:s[0]— одинbyte, число от 0 до 255, здесь первый из двух байтов буквыё. - ✗Ждать
2отlen(s):lenсчитает байты, а не буквы, и каждая буква кириллицы занимает в UTF-8 два байта.
Уточняющие вопросы
- →Почему
for i, r := range sдаёт другие индексы, чемs[i], на UTF-8 тексте? - →В чём разница между
byteиruneв Go?
JuniorТеорияЧастоПочему strings.Builder собирает строку в цикле быстрее, чем +=?
Почему strings.Builder собирает строку в цикле быстрее, чем +=?
Каждый += заново копирует все накопленные байты, а Builder дописывает только новые
Типичные ошибки
- ✗Думать, что Builder заранее знает длину: он растит буфер по мере записи, как
appendрастит срез, а выигрыш в том, что старые байты не копируются при каждой склейке. - ✗Считать, что Builder меняет готовую строку: строки в Go неизменяемы, поэтому Builder копит байты в своём буфере и отдаёт строку через
String.
Уточняющие вопросы
- →Когда для склейки строк хватает обычного
+? - →Что можно делать с нулевым значением
strings.Builderбез всякой подготовки?
JuniorТеорияЧастоЧто выведет программа?
Что выведет программа?
[1 9 3 4] 2
Открыть задачу →Типичные ошибки
- ✗Ждать, что
aостанется[1 2 3 4], будтоa[1:3]создал копию: срез среза делит массив сa, поэтомуb[0] = 9пишет вa[1]. - ✗Ждать, что
len(b)равно3: вa[1:3]конечный индекс не входит, поэтому вbэлементы с индексами 1 и 2.
Уточняющие вопросы
- →Как получить
b, изменения которого не доходят доa? - →Что сделает
b[2] = 9в этой программе?
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ТеорияИногдаВ каком порядке этот цикл обходит пары map?
В каком порядке этот цикл обходит пары map?
Порядок не задан и может меняться от запуска к запуску
Открыть задачу →Типичные ошибки
- ✗Ждать
1 a,2 b,3 c, раз так вышло на одном запуске: порядок не задан, и Go нарочно делает его случайным. - ✗Ждать порядок по ключам, как у
fmt.Println(m):fmtсортирует ключи перед печатью, аrangeнет — сортируйте ключи сами.
Уточняющие вопросы
- →Почему команда 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 распределяет эвакуацию по записям, а не делает её сразу?