День 18 · map: таблица по ключу · страница 4 из 6

Обход map: порядок не гарантирован

Посчитать — полдела: итоги нужно напечатать. Обойти map можно тем же for … range, что и срез, но у обхода map есть свойство, которое ломает отчёты и тесты.


for sku, qty := range stock

Ситуация. Напечатать остатки всех артикулов из map.

Код для чтения · разбираем, набирать не нужно
package main

import "fmt"

func main() {
	stock := map[string]int{
		"P-305": 7,
		"K-417": 12,
		"M-120": 0,
		"A-210": 5,
		"T-118": 3,
	}
	for sku, qty := range stock {
		fmt.Println(sku, qty)
	}
}

Что увидите. Два запуска подряд, программа и данные те же:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day18/scratch$ go run .
M-120 0
A-210 5
T-118 3
P-305 7
K-417 12
stagiaire@lab:~/gocourse/day18/scratch$ go run .
K-417 12
M-120 0
A-210 5
T-118 3
P-305 7

Как это читать. for sku, qty := range stock проходит по всем парам: sku — ключ, qty — значение. Нужен только ключ — for sku := range stock, только значение — for _, qty := range stock.

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

Где порядок не важен — обходите смело: сумма остатков при любом порядке одна.

Код для чтения · разбираем, набирать не нужно
package main

import "fmt"

func main() {
	stock := map[string]int{"K-417": 12, "M-120": 0}
	total := 0
	for _, qty := range stock {
		total += qty
	}
	fmt.Println(total)
}
Вывод · это печатает программа, набирать не нужно
12

Где важен — отчёт, вывод, который сверяют с образцом, тест по строкам, — порядок задаёте вы.


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

Код для чтения · разбираем, набирать не нужно
package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	stock := map[string]int{
		"P-305": 7,
		"K-417": 12,
		"M-120": 0,
		"A-210": 5,
		"T-118": 3,
	}
	for _, sku := range slices.Sorted(maps.Keys(stock)) {
		fmt.Println(sku, stock[sku])
	}
}

Что увидите. Два запуска — один и тот же вывод:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day18/scratch$ go run .
A-210 5
K-417 12
M-120 0
P-305 7
T-118 3
stagiaire@lab:~/gocourse/day18/scratch$ go run .
A-210 5
K-417 12
M-120 0
P-305 7
T-118 3

Как это читать. Изнутри наружу:

Запись Что делает
maps.Keys(stock) отдаёт ключи map по одному — в том же неопределённом порядке
slices.Sorted(…) собирает ключи в срез []string и сортирует по возрастанию
for _, sku := range … обычный обход среза из дня 16: _ — номер, sku — ключ
stock[sku] значение берём из map по ключу
"maps", "slices" в import пакеты стандартной библиотеки

slices.Sorted(maps.Keys(m)) пишите целиком, как одну форму — как форму таблицы тестов в дне 14. Что возвращает maps.Keys, в курсе будет позже. Если ключи уже лежат в срезе, его сортирует slices.Sort из дня 17.

Справка сама предупреждает о порядке:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day18/scratch$ go doc maps.Keys
package maps // import "maps"

func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K]
    Keys returns an iterator over keys in m. The iteration order is not
    specified and is not guaranteed to be the same from one call to the next.

Скобки [Map ~map[K]V, …] пока не разбираем: они значат «map с любыми ключами и значениями». Важна фраза под заголовком: порядок не задан.


Попробуйте сейчас: печать по возрастанию.

Цель: увидеть разный порядок своими глазами и получить одинаковый вывод связкой.

1. Наберите в черновик программу с обходом range stock:

✎ Наберите в файл scratch/main.go
package main

import "fmt"

func main() {
	stock := map[string]int{
		"P-305": 7,
		"K-417": 12,
		"M-120": 0,
		"A-210": 5,
		"T-118": 3,
	}
	for sku, qty := range stock {
		fmt.Println(sku, qty)
	}
}

2. Запустите три раза и сравните:

▶ Выполните
cd ~/gocourse/day18/scratch
go run .
go run .
go run .

3. Замените обход связкой и запустите дважды:

✎ Наберите в файл scratch/main.go
package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	stock := map[string]int{
		"P-305": 7,
		"K-417": 12,
		"M-120": 0,
		"A-210": 5,
		"T-118": 3,
	}
	for _, sku := range slices.Sorted(maps.Keys(stock)) {
		fmt.Println(sku, stock[sku])
	}
}
▶ Выполните
go run .
go run .

4. Прочитайте справку по обеим частям связки:

▶ Выполните
go doc maps.Keys
go doc slices.Sorted

Готово, когда: на шаге 2 порядок хотя бы раз отличался (если нет — запустите ещё), на шаге 3 оба вывода начинаются с A-210, пункт t_doc зелёный.


Попробуйте сейчас: задание tally.

Цель: программа ~/gocourse/day18/tally считает коробки по артикулам через map и печатает итоги по возрастанию артикула.

1. Прочитайте условие — артикулы у вас свои:

▶ Выполните
cd ~/gocourse/day18/tally
cat TASK.txt

2. Замените заготовку main.go своей программой: чтение до конца ввода и TrimSpace — страница 3, пустую строку пропустить continue, счётчик counts[sku]++, печать — связкой и fmt.Printf, число разных артикулов — len(counts).

3. Запустите на примере дважды и на пустом вводе:

▶ Выполните
go run . < example.txt
go run . < example.txt
printf '' | go run .

4. Закоммитьте:

▶ Выполните
cd ~/gocourse/day18
git add .
git commit -m "tally: подсчёт через map"

Готово, когда: оба запуска совпадают с выводом из TASK.txt, пустой ввод печатает Артикулов: 0 — пункт tally. Проверка подаст другие артикулы в другом порядке.


Попробуйте сейчас: задание group.

Цель: в ~/gocourse/day18/group функция totals складывает приход по артикулам, а программа печатает итоги по возрастанию артикула.

1. Прочитайте условие и готовое чтение ввода:

▶ Выполните
cd ~/gocourse/day18/group
cat TASK.txt
cat main.go

2. В totals.go вместо return nil — тело функции: пустая map, цикл по skus с номером, +=, как в «Сумме по группе» на странице 3, и return.

3. В конце main уберите временную строку и напишите печать: связкой по ключам sums, по строке на артикул, и сумму всех значений последней строкой. В import добавьте "maps" и "slices".

4. Запустите дважды и закоммитьте:

▶ Выполните
go run . < example.txt
go run . < example.txt
cd ~/gocourse/day18
git add .
git commit -m "group: totals и печать"

Готово, когда: оба запуска совпадают с выводом из TASK.txt, артикул с количеством 0 в нём есть — пункт group. Тест к totals — на следующей странице.


Что может пойти не так

Что видите Что это значит Что делать
cannot use sku (variable of type int) as string value in map index for sku := range slices.Sorted(…): у среза первым идёт номер for _, sku := range …
in call to slices.Sorted, type map[string]int of stock does not match iter.Seq[E] (cannot infer E) в slices.Sorted передали саму map slices.Sorted(maps.Keys(stock))
undefined: maps в import нет "maps" добавить "maps", так же "slices"
первый запуск совпал с образцом, второй — нет печать прямо из range по map обходить отсортированные ключи
проверка: строки те же, но в другом порядке то же самое на скрытом вводе связка slices.Sorted(maps.Keys(…))

Дальше: map, которую забыли создать, map в функции и тест, — course next