Shanraq.org Shanraq.org
Словарь (map) в Go: ключ и значение
IT

Go: с нуля до своего блога Урок 9 из 50

Словарь (map) в Go: ключ и значение

Девятый урок курса по Go. Словарь, он же map: пара «ключ — значение», по которой находят сразу, а не перебором. Как записать и прочитать, почему отсутствующий ключ отдаёт ноль, а не ошибку, что такое запятая-ok, зачем Go меняет порядок обхода и чем nil-словарь опаснее nil-среза.

Зачем это нужно

В прошлом уроке список статей рос как надо. Но найти в нём статью по адресу — значит пройти его целиком: сто статей — сто сравнений, тысяча — тысяча. А отвечать браузеру нужно сразу.

Для этого в Go есть второй способ хранить много значений — словарь, в коде map. По-русски его называют и картой, и мапой; в коде он всегда map, и именно это слово стоит в документации и в сообщениях об ошибках.

Образ. Словарь. Вы не читаете его с первой страницы до нужного слова — вы открываете сразу на нужной букве. Список ищет перебором, словарь отвечает сразу.

Сразу целиком

Новая папка, go mod init sabaq08, main.go:

package main

import (
	"fmt"
	"slices"
)

func main() {
	views := map[string]int{
		"go":      120,
		"казахский": 45,
		"веб":     30,
	}

	views["go"]++
	views["степь"] = 7

	fmt.Println("тегов:", len(views))
	fmt.Println("go:", views["go"])
	fmt.Println("музыка:", views["музыка"])

	n, ok := views["музыка"]
	fmt.Println("значение:", n, "· есть такой ключ:", ok)

	keys := make([]string, 0, len(views))
	for k := range views {
		keys = append(keys, k)
	}
	slices.Sort(keys)

	for _, k := range keys {
		fmt.Println(k, views[k])
	}
}

Прежде чем запускать — не заглядывая ниже, скажите вслух, что она напечатает. Потом запустите и сверьте: расхождение и есть то место, где вы ещё не знаете языка.

go run . печатает:

тегов: 4
go: 121
музыка: 0
значение: 0 · есть такой ключ: false
go 121
веб 30
казахский 45
степь 7

Тега «музыка» мы не заводили — и всё равно получили ответ.

Разбор

map[string]int — от чего к чему

Запись читается слева направо: в квадратных скобках тип ключа, сразу за ними — тип значения. map[string]int — от строки к числу: по тегу число просмотров.

Ключом может быть не что угодно, а только то, что можно сравнить на равенство: строка, число, bool. Срез ключом быть не может — ровно потому, что два среза Go сравнивать отказывается.

Записать и прочитать

views["степь"] = 7      // записали
n := views["go"]       // прочитали
views["go"]++          // прочитали, прибавили, записали обратно

Скобки те же, что у среза, но внутри не номер, а ключ. Никакого append не нужно: присваивание по новому ключу и есть добавление.

Литерал в начале программы — то же самое, только короче: пары через двоеточие, и запятая в конце каждой строки, включая последнюю.

Ключа нет — это не ошибка, а ноль

views["музыка"] напечатал 0, хотя такого тега не существует. Go не остановил программу и не пожаловался: чтение отсутствующего ключа даёт нулевое значение того типа, что стоит в объявлении. Для int это 0, для string — пустая строка, для boolfalse.

Образ. Пустая полка на складе. Вы спросили полку «музыка» — вам не сказали «такой нет», вам показали пустую полку. Ничего на ней нет, но и ошибки нет.

На этом держится счётчик из задания: m[tag]++ работает и для тега, которого ещё не было, — он просто начинает с нуля.

Запятая-ok: спросить, есть ли ключ

Иногда разница важна: ноль просмотров и «тега нет» — разные вещи. Тогда у словаря спрашивают двумя переменными:

n, ok := views["музыка"]   // 0, false

Второе значение — true, если ключ есть, и false, если нет. Эту форму называют «запятая-ok», и вы встретите её ещё не раз.

Порядок обхода не обещан

range по словарю обходит все пары, но порядок каждый раз может оказаться другим. Это не небрежность, а решение: Go нарочно начинает обход с разного места, чтобы никто не написал код, который держится на порядке.

Проверьте сами: обойдите словарь без сортировки и запустите программу десять раз подряд — вы увидите несколько разных порядков.

Образ. Колода, которую тасуют перед каждой раздачей. Карты те же самые — но кто строил расчёт на их порядке, тот ошибётся.

Нужен порядок — соберите ключи в срез и отсортируйте, как в программе выше. slices.Sort живёт в пакете slices из стандартной библиотеки — имя вам знакомо с прошлого урока. И обратите внимание на make([]string, 0, len(views)): длина ноль, ёмкость известна заранее, ни одного переезда.

delete и len

len(views) — сколько в словаре пар. delete(views, "веб") — убрать пару; если такого ключа нет, ничего не произойдёт, и это тоже не ошибка.

nil-словарь: читать можно, писать нельзя

Здесь словарь ведёт себя не так, как срез, и разницу стоит запомнить сразу.

var m map[string]int
fmt.Println(m["go"])   // 0 — читать можно
m["go"] = 1            // panic: assignment to entry in nil map

В прошлом уроке var titles []string спокойно принимал append. А var m map[string]int записи не принимает: программа останавливается. Словарь нужно создать — make(map[string]int) или литералом map[string]int{}.

Карта урока

Карта урока: ключ отдаёт значение, а отсутствующий ключ — ноль

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Почему views["музыка"] вернул ноль, а не остановил программу? Как отличить «ноль просмотров» от «такого тега нет»?
  2. Зачем Go нарочно меняет порядок обхода словаря?
  3. var titles []string работает с append, а var m map[string]int падает на первой же записи. В чём разница?

Задание

Обязательное. Напишите функцию count(tags []string) map[string]int, которая считает, сколько раз встретился каждый тег. Вызовите её для []string{"go", "веб", "go", "степь", "go", "веб"} и напечатайте результат в алфавитном порядке ключей — сам словарь вам этого порядка не даст.

По желанию.

  • has(m map[string]int, k string) bool — через запятую-ok.
  • Удалите ключ через delete и напечатайте len до и после.
  • Попробуйте записать в var m map[string]int и прочитайте сообщение об аварии целиком. Уметь читать panic — половина отладки.

Куда это встанет в блоге

Маршруты — это словарь: по адресу найти обработчик, не перебирая все. Счётчик просмотров по тегам — словарь. Три языка одной статьи — тоже словарь: по ключу kz, ru или en лежит свой текст, ровно так устроен сайт, который вы сейчас читаете. В уроке про структуры значением станет не число, а целая статья.

Соберите свой блог. Здесь два последних урока встречаются: срез держит порядок статей на главной, а словари — заголовки и объём по адресу. Ни один из двух не смог бы заменить другой, и в step-3 это видно построчно.

Ответы

Показать ответы
  1. Потому что чтение отсутствующего ключа в Go даёт нулевое значение — для int это ноль. Отличить одно от другого можно формой с двумя переменными: n, ok := views["музыка"], где ok будет false, если ключа нет.
  2. Чтобы никто не полагался на порядок, которого словарь не обещает. Окажись порядок случайно устойчивым, на нём выросли бы программы — и сломались бы при первом же изменении внутреннего устройства. Нужен порядок — соберите ключи в срез и отсортируйте.
  3. Пустой срез готов к append: тот сам заводит хранилище и возвращает новый срез. У словаря append-а нет, запись идёт прямо в хранилище — а его ещё не существует, поэтому программа останавливается. Словарь создают через make или литералом.

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.