Перейти к содержимому
Теория · Map · урок 4 из 9

Обход map

В настоящей телефонной книге имена стоят по алфавиту. Наша книга-map устроена иначе: в ней нет порядка записей. Найти номер по имени это не мешает. Мешает это только тому, кто листает книгу целиком и ждёт, что записи пойдут по порядку.

Цикл range проходит все пары map, но не обязательно в том порядке, в котором их записали. Если напечатать пары в цикле, порядок строк может меняться от запуска к запуску. Результат, который от порядка не зависит, получается одинаковым всегда — например, сумма:

GO
package main

import "fmt"

func main() {
    stock := map[string]int{"pear": 3, "plum": 5, "kiwi": 2}
    total := 0
    for _, n := range stock {
        total += n
    }
    fmt.Println(total) // 10
}

Цикл берёт пары в каком-то порядке, но каждое значение попадает в сумму ровно один раз. Поэтому при любом порядке total равно 3 + 5 + 2, и программа всегда печатает 10.

Три формы цикла

Цикл range по map пишут в трёх формах — в зависимости от того, что нужно из каждой пары.

Ключ и значение

В записи for k, v := range m переменная k получает ключ, а v — значение. Эту форму берут, когда нужно и то и другое, например чтобы напечатать пары.

Только ключи

Если значения не нужны, пишут for k := range m: вторую переменную опускают. Так собирают ключи в срез, как в разделе об обходе по ключам ниже.

Только значения

Если не нужны ключи, на их место ставят _: for _, v := range m. Так первый пример считает сумму. Ключ здесь нельзя опустить, как значение: первая переменная в range всегда получает ключ.

Порядок не задан

Язык не задаёт порядок, в котором приходят пары. Go меняет его намеренно: каждый обход начинается со случайного места. Поэтому два запуска программы и даже два цикла подряд в одной программе могут пройти пары по-разному.

Go делает это специально, чтобы код не зависел от внутреннего устройства map. От порядка не зависят, например:

  • сумма значений;
  • количество пар;
  • наибольшее значение.

Этот вопрос часто задают на собеседовании: «В каком порядке range обходит map?» Ответ: порядок не задан, и Go нарочно делает его случайным. Если нужен стабильный порядок, задайте его сами — об этом следующий раздел.

Обход по ключам

Когда порядок нужен, ключи собирают в срез и сортируют функцией slices.Sort из пакета slices (import "slices"). Потом цикл идёт уже по срезу и читает значения из map по ключу:

GO
stock := map[string]int{"pear": 3, "plum": 5, "kiwi": 2}
keys := make([]string, 0, len(stock))
for k := range stock {
    keys = append(keys, k)
}
slices.Sort(keys)
fmt.Println(keys) // [kiwi pear plum]
for _, k := range keys {
    fmt.Print(k, ":", stock[k], " ") // kiwi:2 pear:3 plum:5
}

Давайте разберём пример по строкам:

  1. make([]string, 0, len(stock)) создаёт пустой срез с ёмкостью на все ключи.
  2. Первый цикл кладёт в него ключи в случайном порядке.
  3. slices.Sort(keys) расставляет их по алфавиту, и Println печатает [kiwi pear plum].
  4. Второй цикл идёт по отсортированному срезу и для каждого ключа читает значение из map.

Что делает slices.Sort:

  • меняет порядок элементов в самом срезе и ничего не возвращает;
  • строки упорядочивает по алфавиту, но заглавные буквы идут раньше строчных;
  • числа упорядочивает по возрастанию;
  • саму map не трогает: сортирует только срез её ключей.

Для среза строк то же делает sort.Strings(keys) из пакета sort.

Печать map целиком

Вызов fmt.Println(stock) печатает map[kiwi:2 pear:3 plum:5]: перед печатью пакет fmt сам сортирует ключи. Цикл range этого не делает.

Частая ошибка

Ключи — числа 1, 2, 3, и от цикла ждут, что пары придут по возрастанию:

GO
names := map[int]string{1: "pear", 2: "plum", 3: "kiwi"}
for k, v := range names { // ✗
    fmt.Println(k, v)
}

Строки выходят в любом порядке, и он может отличаться от запуска к запуску. На маленькой map порядок часто совпадает с ожидаемым, поэтому ошибку долго не замечают. Программа проходит проверку на одном запуске и не проходит на следующем. Соберите ключи в срез и отсортируйте их, как показано выше.

Главное

  • range по map выдаёт пары в незаданном порядке, и Go нарочно делает его случайным.
  • Сумма, число пар или максимум от порядка не зависят и получаются одинаковыми всегда.
  • Для стабильного порядка ключи собирают в срез, сортируют через slices.Sort и читают значения по ключам.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом