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?
ссылка на оригинал статьи https://habr.com/ru/articles/934906/
Добавить комментарий