Golang продолжает развиваться. Изначальные проектные решения ставятся под сомнения, а новые вызовы заставляют язык меняться: дженерики, итераторы, новая имплементация мап. Однако, даже нововведения приходят к нам не такими, как в других языках. Вспомните обсуждения сразу после релиза тех же дженериков. На Go, как мне кажется, в большинстве своём пишут люди, пришедшие из прочих языков, у кого Golang не первый ЯП. Они привыкли к другому подходу работы с абстракциями. И им порой не хватает того, что предлагает язык Гофера. Swiss Tables — попытка быть в тренде.
С вами Кирилл Кузин — ведущий подкастов про IT на канале gIT, где вместе с коллегами по цеху рассматриваем индустрию под разными углами, открывая новые горизонты для вас и самих себя. А работаю ведущим разработчиком в Ви.Tech — IT-дочке ВсеИнструменты.ру. Там мы с командой пишем внутренние системы на Go под задачи бизнеса и по ходу дела разбираемся, как наши инструменты устроены и как реально влияют на процесс разработки.
В этой статье речь пойдёт о новых мапах в версии Go 1.24, реализованных по принципу Swiss Tables — швейцарских таблиц. Попробуем найти ответы на вопросы о том, почему мапы изменились, что лежит в основе новой реализации и как к ней пришли.

Чтобы в этом разобраться, вернёмся в прошлое, когда появилась необходимость изменений внутри самого Go. В статье мы не будем погружаться слишком глубоко. Я также предполагаю, что читатели знакомы со старой реализацией мапы в Golang, и имеют поверхностное представление о том, что собой представляют Swiss Tables как концепция в Rust и C++. Если нет — рекомендую к просмотру мой доклад на Golang Conf X (YouTube, RuTube, VK), где я провёл ретроспективу и разбор швейцарских таблиц, а также сослался на более фундаментальный доклад ребят из Abseil, разработавших новый подход к работе хеш-таблиц («Designing a Fast, Efficient, Cache-friendly Hash Table, Step by Step»).
Однако, всё же акцентируем внимание на нескольких ключевых идеях каноничных швейцарских таблиц, чтобы у нас был общий контекст (все они также описаны в статье Abseil):
Два массива — реализация предполагает массив с ключами и значениями, а также массив с метаданными. Последний хранит в своих ячейках состояния ассоциированных по индексу ключей-значений: ячейка массива с данными занята, свободна или же свободна, но помечена заглушкой (tombstone), что удалена. Последнее состояние позволяет не разрушать цепочки пробирования.
Цепочка пробирования — цепочка значений, выстраиваемая хеш-функцией через состояние массива метаданных при разрешении коллизий. Она требуется, если в ячейке, определённой хеш-функцией, уже есть другое значение с таким же хешем, а новое всё равно надо где-то сохранить. Позволяет не терять конфликтующие ключи и их значения, выстраивая их последовательно друг за другом.
Коллизия — ситуация, при которой два разных ключа имеют один и тот же хеш.
Квадратичное пробирование — метод открытой адресации, через который в швейцарских таблицах разрешены коллизии. Это означает, что при коллизии цепочка пробирования строится с помощью квадратичной функции, подбирающей следующую подходящую ячейку для хранения или поиска значения. Таким образом выстраивается цепочка пробирования по хешу, с которым возникает коллизия, но не последовательно, а по порядку, указанному функцией
Использование SIMD — для ускорения операций с массивом метаданных.
Сконцентрируемся на том, как мы получили именно такой код в исходниках, каким мы его увидели в версии 1.24, на чём он основан и почему реализация в новой версии может показаться неаккуратной. Ведь изначально переход реализации мап на швейцарские таблицы казался отличной идеей. Да и сейчас такой кажется.
Проблемы старой реализации мап
Дело в том, что с первых релизов Go Highload сильно изменился, как и работа интернета в целом. А количество пользователей у сервисов росло бешеными темпами вместе с распространением сетей по планете. Как итог — безумное количество данных и возросшие скорости работы с ними. Golang попал в окно возможностей, потому что позволяет реализовывать ожидания бизнеса в условиях быстро меняющихся потребностей клиентов.
Но не всё так радужно. От года к году начали проявляться болячки текущей реализации мап при работе современного бизнеса, основанного на больших данных и высоких нагрузках:
Неоптимальное использование памяти. Каждая новая аллокация памяти для мапы требует объём в два раза больше текущего. Для больших in-memory кэшей это вызов, потому что с ростом x2 значительная часть памяти в конце концов может оказаться аллоцированной, но неиспользуемой.
Эвакуация данных при росте мапы хоть и выполняется постепенно, но бьёт по CPU пиками нагрузки. В системах с миллионами записей это может привести к ощутимому замедлению работы.
Рост количества данных увеличивает вероятность коллизий, а с ними начинает замедляться работа, в том числе из-за использования метода цепочек при разрешении коллизий в конкретном бакете.
Последовательное пробирование на первом этапе таких операций, как вставка, удаление и изменение данных, — простое и действенное решение, к которому стремится язык. Только вместе с тем меняется скорость работы алгоритма мапы: вместо O(1) получаем O(n) в худшем случае.
Бакеты, из которых состоит мапа, разбросаны по куче, а значит происходит overhead по CPU, так как доступ к памяти — случайный. Приходится скакать по памяти и страдать от кэш-промахов, чтобы найти нужный элемент по ключу. На больших объёмах данных это снова аффектит скорость выполнения программ.
Рост нагрузки на GC при огромном количестве бакетов, особенно в случае удаления данных во время эвакуации.
Возможно, этот список не полон, и его можно уточнить, но остаётся факт — развитие интернета потребовало поиска нового решения. Как следствие — изменения мап. И, как уже понятно, одним из подобных решений стала концепция Swiss Tables, зарекомендовавшая себя в C++. Однако, первыми в публичном поле на неё обратили внимание не разработчики языка Golang, а комьюнити. В августе 2022 появилось предложение по изменению внутреннего механизма работы мапы. И пока идея доковыляла до конечной реализации, то же сообщество постаралось предложить собственное видение того, как могли бы выглядеть реализации Swiss Tables с учётом возможностей языка на тот момент. Рассмотрим две библиотеки:
DoltHub / swiss — первая попытка применить новую идею;
Cockroach / swiss — логичное развитие на основе интерпретации DoltHub.
Я предлагаю изучить каждую из них, чтобы понять, как идея Swiss Tables эволюционировала в Golang и что мы получили в итоге. И начнём с Dolthub.
Swiss Tables в DoltHub
Как выглядит эта мапа через призму швейцарских таблиц:
type Map[K comparable, V any] struct {
ctrl []metadata
groups []group[K, V]
hash maphash.Hasher[K]
resident uint32
dead uint32
limit uint32
}
Это обычная структура, принятая в Golang, но с рядом интересных деталей:
Во-первых, метаданные представлены как слайс массивов, где каждый массив связан со своей группой данных. Как водится, количество пар ключ-значение и битов в метаданных, константа groupSize — восемь:
type metadata [groupSize]int8
Во-вторых, сами данные разбиваются на группы по восемь ключей-значений. Таким образом остаётся соответствие индекса массива с метаданными и индекса группы, к которой массив относится:
type group[K comparable, V any] struct {
keys [groupSize]K
values [groupSize]V
}
В-третьих, самое интересное, ребята не стали реализовывать свою хеш-функцию, а воспользовались уже существующей, вытянув её из внутреннего представления мапы в исходном коде Go. Избыточно говорить, что при таком подходе есть риск со временем поломать зависимости при попытке через интерфейс скопировать один в один нужную структуру и вытащить оттуда то, что вас интересует. Не делайте так. Тем более, что то самое представление уже удалили из исходного кода.
type hashfn func(unsafe.Pointer, uintptr) uintptr
func getRuntimeHasher[K comparable]() (h hashfn) {
a := any(make(map[K]struct{}))
i := (*mapiface)(unsafe.Pointer(&a))
h = i.typ.hasher
return
}
Помимо внутреннего устройства мап, нас в первую очередь интересуют три основные операции, которые реализуются на хеш-таблицах: получение данных (get
), вставка или изменение данных (put
) и их удаление (delete
). Предлагаю начать с операции Get()
.
func (m *Map[K, V]) Get(key K) (value V, ok bool) {
hi, lo := splitHash(m.hash.Hash(key))
g := probeStart(hi, len(m.groups))
for {
matches := metaMatchH2(&m.ctrl[g], lo)
for matches != 0 {
s := nextMatch(&matches)
if key == m.groups[g].keys[s] {
value, ok = m.groups[g].values[s], true
return
}
}
matches = metaMatchEmpty(&m.ctrl[g])
if matches != 0 {
ok = false
return
}
g += 1
if g >= uint32(len(m.groups)) {
g = 0
}
}
}
По факту, тут выполняется несколько этапов работы с данными и самой мапой:
Оптимизация поиска группы, с которой начнём искать нужные данные через старшие биты хеша. Так начинается процесс пробирования. Подобная оптимизация с частью битов хеша есть и в старых мапах.
Поиск совпадений ключей в этой группе — через младшие биты хеша и массив с метаданными, который, помимо битов состояния, хранит часть хеша.
Проверка полученных совпадений по метаданным на то, что имеется совпадение у какого-то конкретного сохраненного в мапе ключа с ключом из запроса.
Если ключ из запроса не найден в группе, алгоритм пытается понять, есть ли пустая ячейка в текущей группе с данными. Если есть, это означает, что цепочка пробирования прервалась, само пробирование завершено и дальше смысла в поиске нет.
Если пустой ячейки нет, то последовательно выбирается следующая группа и так до момента, пока либо алгоритм не пройдёт все группы, либо не найдёт интересующий ключ и значение.
Операция вставки значения выглядит почти также, за исключением первых строчек:
if m.resident >= m.limit {
m.rehash(m.nextSize())
}
Если мапе не хватает места, так как количество данных превысило установленный лимит, происходит её увеличение, то есть — рехеширование. И операция Put()
нам ценна как раз этим.
func (m *Map[K, V]) rehash(n uint32) {
groups, ctrl := m.groups, m.ctrl
m.groups = make([]group[K, V], n)
m.ctrl = make([]metadata, n)
for i := range m.ctrl {
m.ctrl[i] = newEmptyMetadata()
}
m.hash = maphash.NewSeed(m.hash)
m.limit = n * maxAvgGroupLoad
m.resident, m.dead = 0, 0
for g := range ctrl {
for s := range ctrl[g] {
c := ctrl[g][s]
if c == empty || c == tombstone {
continue
}
m.Put(groups[g].keys[s], groups[g].values[s])
}
}
}
При рехешировании создаётся новая мапа. Её ёмкость увеличивается в два раза, а цепочки пробирования пересоздаются. В новую структуру попадают только те ячейки, для которых в массиве метаданных существует пометка в виде бита о том, что ключ/значение правда существуют. Пустая ячейка без значения, но помеченная в качестве заглушки (чтобы не сломать цепочку пробирования), не переносится.
Метод удаления данных рассмотрим лишь частично:
for {
matches := metaMatchH2(&m.ctrl[g], lo)
for matches != 0 {
s := nextMatch(&matches)
if key == m.groups[g].keys[s] {
ok = true
if metaMatchEmpty(&m.ctrl[g]) != 0 {
m.ctrl[g][s] = empty
m.resident--
} else {
m.ctrl[g][s] = tombstone
m.dead++
}
var k K
var v V
m.groups[g].keys[s] = k
m.groups[g].values[s] = v
return
}
}
Здесь и скрывается логика проставления tombstone (заглушек) в цепочках пробирования. Всё снова сводится к тому, заполнена ли группа полностью.
По итогу реализация библиотеки Swiss Tables выглядит довольно просто за счёт структуры, последовательного пробирования по группам и внутри них, а также имеет следующие очевидные плюсы:
load factor (тот самый, который в старых мапах Golang ~82%) — здесь все 87%;
неплохой перенос основных концепций Swiss Tables в плоскость Go;
использование SIMD через ручной код ассемблера.
Но есть и минусы:
очень простая реализация, которая затрагивает при рехешировании весь объём мапы (этим же страдает старая мапа);
отсутствие общности со старой реализацией мап в Go. При желании реализовать нечто похожее в исходном коде придётся изрядно попотеть, чтобы все сущности органично вписались в существующий исходный код.
Теперь предлагаю посмотреть на вторую библиотеку — Swiss Tables от Cockroach.
Swiss Tables в Cockroach: что изменили
Здесь код значительно сложнее, а модель включает несколько уровней: мапу, бакеты, группы и слоты:
type Map[K comparable, V any] struct {
hash hashFn
seed uintptr
allocator Allocator[K, V]
bucket0 bucket[K, V]
dir unsafeSlice[bucket[K, V]]
used int
globalShift uint32
maxBucketCapacity uint32
_ noCopy
}
Эта мапа представляет собой структуру, во многом похожую на то, как мапы выглядели в старой реализации Go. Здесь есть и зерно, и другие технические поля, но ключевые из них следующие:
bucket0 — тот самый бакет, с которого начинается мапа, «нулевой пациент». Разросшись до непотребного размера, он дробится и все его части уезжают в следующее поле
dir
, а полеbucket0
становитсяnil
.dir — слайс, ячейки которого уже помогают общаться с бакетами, когда их больше одного. Будем называть каталогом бакетов.
used — количество занятых ячеек в мапе по всем бакетам.
Особняком стоит поле hash, в котором живёт хеш-функция. Реализовали ли Cockroach свою? Нет, она получена тем же способом, что и в DoltHub, о чём в коде оставлено прямое замечание:
// https://github.com/dolthub/maphash provided the
// inspiration and general implementation technique.
func getRuntimeHasher[K comparable]() hashFn {
a := any((map[K]struct{})(nil))
return (*rtEface)(unsafe.Pointer(&a)).typ.Hasher
}
Преемственность, как она есть. И не сказать, чтобы хорошая. Дальше перейдём на уровень ниже — к бакетам.
type bucket[K comparable, V any] struct {
groups unsafeSlice[Group[K, V]]
groupMask uint32
capacity uint32
used uint32
growthLeft uint32
localDepth uint32
index uint32
}
По факту, бакет — это та же мапа в понимании библиотеки Dolthub, но с более сложной логикой разрешения коллизий. Сами данные хранятся глубже — в группах, а бакет содержит информацию, которая помогает с ними работать. Группа — простая структура: один массив метаданных из восьми ячеек и массив из восьми слотов, каждый из которых хранит пару ключ-значение:
type Group[K comparable, V any] struct {
ctrls ctrlGroup
slots slotGroup[K, V]
}
type slot[K comparable, V any] struct {
key K
value V
}
Заметно, что идеи Dolthub преобразились, усложнились и теперь работают иначе, визуально более приближенно к тому, что из себя представляли мапы до версии 1.24. Но возникает закономерный вопрос — каков механизм роста мапы? В старой реализации и в реализации Dolthub рост затрагивал мапу целиком. Тут, обращаясь к документации, механизм иной. Он называется Extendible Hashing и работает с полем dir
.
Разберёмся, что из себя представляет этот каталог бакетов:

Представим, что в мапе есть три бакета. И каждый имеет доселе неизведанную характеристику — localDepth, или локальную глубину (уж простите меня за дословный перевод). По сути — это счётчик того, сколько раз данный бакет и его потомки/родители разделялись надвое. У самой мапы есть свойство globalDepth, или глобальная глубина, которая подсказывает максимальную локальную глубину бакетов. Другими словами — сколько раз дробился самый используемый бакет (и его потомки). Здесь работает несколько правил:
глобальная глубина не может быть меньше максимальной локальной глубины, поэтому обязательно возможности инкрементируется, если максимальная локальная глубина становится больше по значению;
количество ячеек в
dir
равно степени двойки и длина адреса ячейки равна глобальной глубине;на один бакет может указывать более одной ячейки, при этом только через одну из них алгоритм может изменять бакет, а через остальные только читать данные из него.
Если в последний бакет добавить ещё данных, превысив их количество, которое он может содержать, бакет разобьётся надвое.

Локальная глубина бакета и его нового собрата увеличится на единицу, как и глобальная глубина. Dir
увеличится по степени двойки и перераспределит указатели ячеек на бакеты по новому. Вот и весь механизм роста. Он потрясающ тем, что не затрагивает мапу целиком, а только одну её часть.
Теперь перейдём к реализации методов. Delete()
и Get()
опустим — они по логике похожи на то, что мы уже видели у Dolthub. А у метода Put()
интересно рассмотреть, как он рехеширует бакет программно.
func (b *bucket[K, V]) rehash(m *Map[K, V]) {
if b.capacity > groupSize && b.tombstones() >= b.capacity/3 {
b.rehashInPlace(m)
return
}
newCapacity := 2 * b.capacity
if newCapacity > m.maxBucketCapacity {
b.split(m) return
}
b.resize(m, newCapacity)
}
Ребята из Cockroach реализовали целых три стратегии рехеширования:
Первая стратегия предлагает перестроить существующий бакет, если количество заглушек внутри него более трети от всей ёмкости. Эта оптимизация ведёт к тому, что существующий бакет перестраивает цепочки пробирования, избавляясь от неиспользуемых для хранения данных заглушек. Считаю, что ход отличный.
Вторая стратегия предлагает разделение бакета пополам в случае, если достигнут лимит по максимальной ёмкости.
Третий классический ход — увеличить ёмкость бакета в два раза.
На моём оборудовании (Mac Air M4 16 Gb), при работе с мапой на строках в лёгком (6 элементов), среднем (> 8 тысяч элементов) и тяжёлом весе (> 4 млн. элементов), реализация Cockroach выиграла и у Dolthub, и у старых гошных мап по скорости работы. По памяти, к сожалению, тесты не проводил.
Результаты при малом количестве кэш-промахов:
Dolthub |
Cockroach |
Go-Maps |
|
String, 6 эл. |
14.57 ns/op |
|
11.29 ns/op |
String, 8192 эл. |
20.69 ns/op |
|
37.38 ns/op |
String, 4 194 304 эл. |
92.67 ns/op |
|
81.31 ns/op |
Результаты при большом количестве кэш-промахов, что наиболее ценно:
Dolthub |
Cockroach |
Go-Maps |
|
String, 6 эл. |
10.26 ns/op |
|
9.023 ns/op |
String, 8192 эл. |
13.20 ns/op |
|
15.75 ns/op |
String, 4194304 эл. |
|
43.34 ns/op |
70.84 ns/op |
Единственный минус у Cockroach — это отсутствие SIMD, но в Golang это можно исправить так же, как это сделали в DoltHub.
Что попало в Go 1.24
Так как логика библиотеки Cockroach очень схожа с тем, что уже есть в исходном коде языка, то она и легла в основу новых мап в Golang. В этом и заключается прелесть: разработчики языка по сути переиспользовали наиболее удачный вариант от комьюнити, слегка доработав его под свои ограничения. Всё осталось таким же: и иерархия (только бакеты поменяли на таблицы); и работа с разделением бакетов — один в один. Даже код методов одинаков. Посмотрите сами на эту мапу:
type Map struct {
used uint64
seed uintptr
dirPtr unsafe.Pointer
dirLen int
globalDepth uint8
globalShift uint8
writing uint8
clearSeq uint64
}
Отличия есть, но концептуально — это Cockroach. И именно поэтому я предложу вам сконцентрироваться не на исходном коде новых мап, потому что эволюцию мы уже проследили и понимаем, как они выглядят, а на интересные решения внутри.
И буду честен — готовясь к конференции, я ещё не до конца понимал причины принятия тех или иных решений. Сейчас уже могу ответить на некоторые вопросы «почему». А на остальные приглашаю ответить в комментариях.
Во-первых, по неведомой мне изначально причине, код мап разбит на несколько пакетов с путями:
src/runtime/map_noswiss.go
src/runtime/map_swiss.go
src/internal/runtime/maps/runtime_swiss.go
И вроде бы логично, что мы оставляем старую реализацию для сохранения обратной совместимости. Но почему новая мапа разъехалась на два файла — немного не понятно. А на самом деле, она разъехалась на все три. Есть ещё src/internal/runtime/maps/map.go
.
При этом, если посмотреть в исходный код методов, то удивляет, что методы Get()
и Put()
описаны в одном месте, а логика Delete()
тянется из последнего упомянутого мной файла (в котором, кстати, есть реализация и прочих методов, впрочем, неиспользуемых).
К этому у меня был большой вопрос. И по поводу кода методов Get()
и Put()
этот вопрос остаётся — зачем? А вот на счёт разных файлов в отдельных частях кодовой базы всё немного понятнее. Причин для такого решения несколько. И одна из них, как мы любим, историческая. Связана с директивой go:linkname
, которую использовали некоторые библиотеки, чтобы залезть во внутренности рантайма. Теперь просто так не поменять контракты и реализацию. Приходится постепенно вводить изменения, максимально их контролируя и вынося из ядра языка.
Также, как известно, пакет runtime компилируется по особому, и, в частности, не использует race-detector. Старые мапы жили самодостаточно и без него — как и без тестов. Чилили в рантайме. Но, кажется, что разработчики языка решили изменить подход, вынеся отдельную логику во вне, чтобы иметь возможность тестировать изменения и контролировать ключевые аспекты работы новых мап. А уже их реализация будет подключаться и в рантайм (метод Delete(), как пример), и в будущем в остальные части исходного кода. Скорее всего это будет вообще отдельная библиотека, доступная извне.
Как видно, причин сделать некоторое количество файлов в разных частях кода, да ещё где-то через go:linkname к старой реализации — вынужденный компромисс, ввиду особенностей развития Golang. И подобная топология точно должна со временем измениться.
Ещё одна странность — вольность с константами. Разработчики взяли одну из важнейших вещей в виде ограничения на максимальную ёмкость таблиц maxTableCapacity
, скажем так, с потолка. Обычно на ревью за такое бьют по рукам. Более странно, что с такой опцией мапы включены по дефолту. Для себя я не нашёл никаких объяснений на этот счёт. Может быть, вы можете подсказать, зачем было делать именно так?
И это ещё не всё. Методы keys и values остались висеть не имплементированными. Судя по issue, функции заезжали для работы с итераторами, но в конечном итоге убрали их реализацию и пока не почистили окончательно.
Теперь посмотрим на рехеширование внутри новой реализации мап:
func (t *table) rehash(typ *abi.SwissMapType, m *Map) {
newCapacity := 2 * t.capacity
if newCapacity <= maxTableCapacity {
t.grow(typ, m, newCapacity)
return
}
t.split(typ, m)
}
К сожалению, здесь не нашлось места для рехеширования по месту — механизма, который учитывает количество заглушек внутри конкретной таблицы, как это реализовано у Cockroach. Разработчики честно признаются в комментариях к коду и в issue, что такой подход трудно реализовать.
Причина в семантике range
при обходе мапы: итерации гарантируют, что каждое значение будет возвращено один раз, а порядок обхода остаётся псевдослучайным. RehashInPlace
сломало бы эту гарантию. Изменив порядок хранения ключей, оно могло бы перемещать ещё не возвращённые значения в уже пройденную область и наоборот, что приводило бы к повторной выдаче одного и того же элемента. Поэтому, чтобы не ломать сам язык, от такого подхода пока отказались: его реализация сейчас слишком сложна.
Другие отличия Swiss Tables от канонической реализации
А чем ещё Swiss Tables в Golang отличается от канонической реализации? Прежде всего тем, что SIMD поддерживается лишь частично. В рантайме и стандартной библиотеке можно найти SIMD-инструкции, но это пока только ручной ассемблер, который, при желании, можно написать самому — и для x86, и для Neon на ARM. Но полноценной поддержки таких оптимизаций, особенно для новых мап, пока нет. Здесь есть куда расти.
Поэтому если вдруг вы не захотите использовать новые мапы и продолжите работать со старым, проверенным инструментом, то можете указать глобальную переменную GOEXPERIMENT=noswissmap
, и будет вам счастье. Никаких новых неизвестных багов. Только старые и известные.
Бенчмарки: кто быстрее
Сравним мапы между собой. На моей аппаратуре новые мапы стали пошустрее старых, однако, они всё ещё проигрывают Cockroach. Вот результаты с малым количеством кэш-промахов:
Swiss |
Cockroach |
Go-Maps |
|
String, 6 эл. |
|
10.85 ns/op |
11.29 ns/op |
String, 8192 эл. |
16.00 ns/op |
|
37.38 ns/op |
String, 4194304 эл. |
129.5 ns/op |
|
81.31 ns/op |
Тут с большим количеством кэш-промахов:
Swiss |
Cockroach |
Go-Maps |
|
String, 6 эл. |
11.49 ns/op |
|
9.023 ns/op |
String, 8192 эл. |
13.11 ns/op |
|
15.75 ns/op |
String, 4194304 эл. |
49.54 ns/op |
|
70.84 ns/op |
Оглядываясь на всё выше написанное — во многом повторяющее мой доклад на Golang Conf — не могу сказать, что релиз получился совсем уж сырым. Шероховатости есть, как и вопросы к реализации, но в целом это обычная инженерная задача: внести изменения в уже работающую систему, не сломав её. На процесс повлияли и окружение, и фраза «так исторически сложилось». Так что в этой статье я говорю тому себе, что выступал на конференции — нормально, и так сойдёт!
При этом очевидно, что у разработчиков языка есть огромный потенциал для оптимизации и дальнейшей модернизации швейцарских таблиц. Как минимум, нужно догнать библиотеку Cockroach в быстродействии. Так что главный вопрос остаётся открытым: готовы ли мы доверять новой версии мап прямо сейчас? Или пока предпочтём остаться на старой реализации?
Как бы то ни было, язык развивается, следит за трендами и старается идти в ногу со временем. И я уверен, что команда разработки допилит всё, что сейчас не очень нравится комьюнити. В конце концов в этом и смысл. Правда?
А мы теперь понимаем, откуда получили то, что получили, какие этапы осмысления прошли швейцарские таблицы до того, как попали в исходный код. На этом спасибо за ваше внимание. И у меня остался к вам ещё один вопрос: нравятся новые мапы в Go?
sl4mmer
Лично по моим ощущениям, стоило бы еще подержать в экспериментал и допилить реализацию