Skip to content

Отображения в Go

Обычно реализации отображений используют два подхода: хэш-таблицы (неупорядоченные) и поисковые деревья (упорядоченные). В Go map реализован на основе хэш-таблицы, поэтому неупорядочен. В этой статье мы не будем углубляться в реализацию — это выходит за рамки основ.

TIP

Подробнее о реализации: Реализация map

Инициализация

В Go ключи map должны быть сравнимыми: string, int — сравнимы, []int — нет, поэтому не может быть ключом.

Способы инициализации:

Литерал:

go
map[keyType]valueType{}

Примеры:

go
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:

Принимает тип и начальную ёмкость:

go
mp := make(map[string]int, 8)

mp := make(map[string][]int, 10)

Map — ссылочный тип. Неинициализированный map доступен, но не может хранить элементы — требуется выделение памяти:

go
func main() {
   var mp map[string]int
   mp["a"] = 1
   fmt.Println(mp)
}
panic: assignment to entry in nil map

TIP

При инициализации рекомендуется выделять разумную ёмкость для уменьшения количества расширений.

Доступ

Доступ к элементам аналогичен индексации массива:

go
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 возвращает два значения: значение и булево значение существования ключа:

go
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("ключ не существует")
   }
}

Получение длины:

go
func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   fmt.Println(len(mp))
}

Сохранение значений

Сохранение аналогично массивам:

go
func main() {
   mp := make(map[string]int, 10)
   mp["a"] = 1
   mp["b"] = 2
   fmt.Println(mp)
}

При использовании существующего ключа значение перезаписывается:

go
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()

go
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:

go
func delete(m map[Type]Type1, key Type)

Пример:

go
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 удаление не работает:

go
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:

go
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 1

NaN доступен через обход:

go
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 требовалось удалять каждый ключ:

go
func main() {
  m := map[string]int{
    "a": 1,
    "b": 2,
  }
  for k, _ := range m {
    delete(m, k)
  }
  fmt.Println(m)
}

В Go 1.21 добавлена функция clear:

go
func main() {
  m := map[string]int{
    "a": 1,
    "b": 2,
  }
  clear(m)
  fmt.Println(m)
}

Вывод:

map[]

Set

Set — неупорядоченная коллекция уникальных элементов. В Go нет встроенного Set, но map подходит:

go
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:

go
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.

Golang by www.golangdev.cn edit