Отображения в Go
Обычно реализации отображений используют два подхода: хэш-таблицы (неупорядоченные) и поисковые деревья (упорядоченные). В Go map реализован на основе хэш-таблицы, поэтому неупорядочен. В этой статье мы не будем углубляться в реализацию — это выходит за рамки основ.
TIP
Подробнее о реализации: Реализация map
Инициализация
В Go ключи map должны быть сравнимыми: string, int — сравнимы, []int — нет, поэтому не может быть ключом.
Способы инициализации:
Литерал:
map[keyType]valueType{}Примеры:
mp := map[int]string{
0: "a",
1: "a",
2: "a",
3: "a",
4: "a",
}
mp := map[string]int{
"a": 0,
"b": 22,
"c": 33,
}Функция make:
Принимает тип и начальную ёмкость:
mp := make(map[string]int, 8)
mp := make(map[string][]int, 10)Map — ссылочный тип. Неинициализированный map доступен, но не может хранить элементы — требуется выделение памяти:
func main() {
var mp map[string]int
mp["a"] = 1
fmt.Println(mp)
}panic: assignment to entry in nil mapTIP
При инициализации рекомендуется выделять разумную ёмкость для уменьшения количества расширений.
Доступ
Доступ к элементам аналогичен индексации массива:
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
fmt.Println(mp["a"])
fmt.Println(mp["b"])
fmt.Println(mp["d"])
fmt.Println(mp["f"])
}Вывод:
0
1
3
0Для несуществующего ключа возвращается нулевое значение. Map возвращает два значения: значение и булево значение существования ключа:
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
if val, exist := mp["f"]; exist {
fmt.Println(val)
} else {
fmt.Println("ключ не существует")
}
}Получение длины:
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
fmt.Println(len(mp))
}Сохранение значений
Сохранение аналогично массивам:
func main() {
mp := make(map[string]int, 10)
mp["a"] = 1
mp["b"] = 2
fmt.Println(mp)
}При использовании существующего ключа значение перезаписывается:
func main() {
mp := make(map[string]int, 10)
mp["a"] = 1
mp["b"] = 2
if _, exist := mp["b"]; exist {
mp["b"] = 3
}
fmt.Println(mp)
}Особый случай: math.NaN()
func main() {
mp := make(map[float64]string, 10)
mp[math.NaN()] = "a"
mp[math.NaN()] = "b"
mp[math.NaN()] = "c"
_, exist := mp[math.NaN()]
fmt.Println(exist)
fmt.Println(mp)
}Вывод:
false
map[NaN:c NaN:a NaN:b]Одинаковые ключи не перезаписываются, существует несколько записей, невозможно получить значение. Это связано со стандартом IEEE754 и инструкцией UCOMISD: любое число не равно NaN, NaN не равен самому себе. Хэш каждый раз разный. Сообщество обсуждало это, но官方 считает изменения излишними. Избегайте использования NaN в качестве ключа.
Удаление
Функция delete:
func delete(m map[Type]Type1, key Type)Пример:
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
fmt.Println(mp)
delete(mp, "a")
fmt.Println(mp)
}Вывод:
map[a:0 b:1 c:2 d:3]
map[b:1 c:2 d:3]Для NaN удаление не работает:
func main() {
mp := make(map[float64]string, 10)
mp[math.NaN()] = "a"
mp[math.NaN()] = "b"
mp[math.NaN()] = "c"
fmt.Println(mp)
delete(mp, math.NaN())
fmt.Println(mp)
}Вывод:
map[NaN:c NaN:a NaN:b]
map[NaN:c NaN:a NaN:b]Обход
Использование for range:
func main() {
mp := map[string]int{
"a": 0,
"b": 1,
"c": 2,
"d": 3,
}
for key, val := range mp {
fmt.Println(key, val)
}
}Вывод (неупорядочен):
c 2
d 3
a 0
b 1NaN доступен через обход:
func main() {
mp := make(map[float64]string, 10)
mp[math.NaN()] = "a"
mp[math.NaN()] = "b"
mp[math.NaN()] = "c"
for key, val := range mp {
fmt.Println(key, val)
}
}Вывод:
NaN a
NaN c
NaN bОчистка
До Go 1.21 требовалось удалять каждый ключ:
func main() {
m := map[string]int{
"a": 1,
"b": 2,
}
for k, _ := range m {
delete(m, k)
}
fmt.Println(m)
}В Go 1.21 добавлена функция clear:
func main() {
m := map[string]int{
"a": 1,
"b": 2,
}
clear(m)
fmt.Println(m)
}Вывод:
map[]Set
Set — неупорядоченная коллекция уникальных элементов. В Go нет встроенного Set, но map подходит:
func main() {
set := make(map[int]struct{}, 10)
for i := 0; i < 10; i++ {
set[rand.Intn(100)] = struct{}{}
}
fmt.Println(set)
}Вывод:
map[0:{} 18:{} 25:{} 40:{} 47:{} 56:{} 59:{} 81:{} 87:{}]TIP
Пустая структура не занимает памяти.
Внимание
Map не является потокобезопасным. Go Team считает, что большинство сценариев не требуют высокой конкурентности, а мьютексы снижают производительность. В map есть детектор конфликтов, вызывающий fatal error:
func main() {
group.Add(10)
mp := make(map[string]int, 10)
for i := 0; i < 10; i++ {
go func() {
for i := 0; i < 100; i++ {
mp["helloworld"] = 1
}
for i := 0; i < 10; i++ {
fmt.Println(mp["helloworld"])
}
group.Done()
}()
}
group.Wait()
}fatal error: concurrent map writesВ таких случаях используйте sync.Map.
