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

nil-map, map в функции и тест

Три места, где спотыкаются чаще всего: map объявлена, но не создана; map передана в функцию; тест сравнивает map.


Объявлена, но не создана

Ситуация. Счётчик объявлен, как когда-то накопитель: через var, без начального значения.

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

import "fmt"

func main() {
	var counts map[string]int
	fmt.Println(counts == nil, len(counts), counts["K-417"])
	counts["K-417"]++
	fmt.Println(counts)
}

Что увидите.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day18/scratch$ go run .
true 0 0
panic: assignment to entry in nil map

goroutine 1 [running]:
main.main()
	/home/stagiaire/gocourse/day18/scratch/main.go:8 +0xc5
exit status 2

Как это читать. var counts map[string]int кладёт нулевое значение для map — nil, «map нет». Таблицы, куда писать, не существует.

Лечится созданием map до первой записи: counts := map[string]int{} или make(map[string]int).


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

Цель: программа в ~/gocourse/day18/fix1 перестаёт падать и считает коробки.

1. Прочитайте комментарий в начале main.go и запустите:

▶ Выполните
cd ~/gocourse/day18/fix1
go run . < example.txt

2. По номеру строки из паники найдите запись в map, а по ней — объявление. Почините объявление: одна строка.

3. Проверьте и закоммитьте:

▶ Выполните
go run . < example.txt
diff ~/.course/orig/day18/fix1/main.go main.go
cd ~/gocourse/day18
git add .
git commit -m "fix1: map создана"

Готово, когда: программа печатает две строки, diff показывает одну-две строки — пункт fix1.


map в функции

Функция получает копию аргумента — правило дня 11. Что копируется, когда аргумент — map? Сначала предскажите.

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

Цель: предсказать два числа и сдать ответ.

1. Не запуская, прочитайте программу. reserve уменьшает на единицу и остаток в map, и число left.

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

import "fmt"

func reserve(stock map[string]int, left int) {
	stock["K-417"]--
	left--
}

func main() {
	stock := map[string]int{"K-417": 5}
	left := 5
	reserve(stock, left)
	reserve(stock, left)
	fmt.Println(stock["K-417"], left)
}

2. Сдайте два числа через пробел:

▶ Выполните · выделенное замените своим
course answer day18.q4 ЧИСЛО ЧИСЛО

3. Проверьте запуском:

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

import "fmt"

func reserve(stock map[string]int, left int) {
	stock["K-417"]--
	left--
}

func main() {
	stock := map[string]int{"K-417": 5}
	left := 5
	reserve(stock, left)
	reserve(stock, left)
	fmt.Println(stock["K-417"], left)
}
▶ Выполните
cd ~/gocourse/day18/scratch
go run .

Вывод и разбор — под блоком.

Готово, когда: пункт q4 в course check 18 зелёный.


Что увидите.

Вывод · это печатает программа, набирать не нужно
3 5

Как это читать. В left лежит само число, и функция уменьшала свою копию. В переменной map лежит не таблица, а указание на таблицу: копируется указание, таблица остаётся одна.

Образец · набирать не нужно
  main:     stock ──────┐
                        ├──►  ┌─────────┬───┐
  reserve:  stock ──────┘     │ "K-417" │ 3 │
                              └─────────┴───┘

  main:     left = 5          reserve: своя left, 5 → 4, после return её нет

Поэтому запись внутри функции видна в main — как элементы среза в дне 16. Функции, которые только дописывают в переданную map, результата не возвращают. Что будет, если внутри функции присвоить параметру новую map, — в course extra 18.

Ключом map может быть только то, что сравнивается через ==: строка, число, bool. Срез не сравнивается, и программа из строк boxes := map[[]int]string{} и fmt.Println(boxes) не собирается: invalid map key type []int.


Сравнить map целиком

Ситуация. Тест к totals из задания group. Сравнить map через == или !=, как числа в дне 14, нельзя: компилятор отвечает invalid operation: got != want (map can only be compared to nil). Для сравнения целиком есть готовая maps.Equal(a, b) — true, если пары одни и те же; порядок записи неважен:

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

import (
	"fmt"
	"maps"
)

func main() {
	a := map[string]int{"K-417": 12, "M-120": 0}
	b := map[string]int{"M-120": 0, "K-417": 12}
	c := map[string]int{"K-417": 12}
	fmt.Println(maps.Equal(a, b), maps.Equal(a, c))
}
Вывод · это печатает программа, набирать не нужно
true false

Почему не сверить значения по одному? Вот функция, которая теряет артикул с количеством 0:

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

func totals(skus []string, qtys []int) map[string]int {
	result := map[string]int{}
	for i, sku := range skus {
		if qtys[i] == 0 {
			continue
		}
		result[sku] += qtys[i]
	}
	return result
}

Тест по значениям:

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

import "testing"

func TestTotals(t *testing.T) {
	got := totals([]string{"A-1", "C-3"}, []int{2, 0})
	if got["A-1"] != 2 {
		t.Errorf("A-1: получили %d, хотели 2", got["A-1"])
	}
	if got["C-3"] != 0 {
		t.Errorf("C-3: получили %d, хотели 0", got["C-3"])
	}
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day18/scratch$ go test .
ok  	day18/scratch	0.008s

Зелёный: ключа C-3 нет, но чтение отсутствующего ключа дало 0. Тот же тест с maps.Equal:

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

import (
	"maps"
	"testing"
)

func TestTotals(t *testing.T) {
	got := totals([]string{"A-1", "C-3"}, []int{2, 0})
	want := map[string]int{"A-1": 2, "C-3": 0}
	if !maps.Equal(got, want) {
		t.Errorf("totals = %v, хотели %v", got, want)
	}
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day18/scratch$ go test .
--- FAIL: TestTotals (0.00s)
    totals_test.go:12: totals = map[A-1:2], хотели map[A-1:2 C-3:0]
FAIL
FAIL	day18/scratch	0.005s
FAIL

%v печатает map так же, как Println, — видно, какой пары не хватает.


Попробуйте сейчас: тест к totals.

Цель: тест totals_test.go в каталоге ~/gocourse/day18/group ловит обе неверные версии: ту, что перезаписывает количество, и ту, что теряет артикул с нулём.

1. Наберите форму — таблица случаев из дня 14, поле want типа map:

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

import (
	"maps"
	"testing"
)

func TestTotals(t *testing.T) {
	cases := []struct {
		name string
		skus []string
		qtys []int
		want map[string]int
	}{
		{"одна строка", []string{"A-1"}, []int{3}, map[string]int{"A-1": 3}},
		{"пусто", []string{}, []int{}, map[string]int{}},
	}
	for _, c := range cases {
		got := totals(c.skus, c.qtys)
		if !maps.Equal(got, c.want) {
			t.Errorf("%s: totals = %v, хотели %v", c.name, got, c.want)
		}
	}
}

Срез случаев знаком с дня 16; что внутри скобок struct — блок 5.

2. Допишите два случая, по строке на каждый: артикул дважды (ожидаемое — сумма) и артикул с количеством 0 (ожидаемое — пара с нулём). Запустите:

▶ Выполните
cd ~/gocourse/day18/group
go test .
go test -v .

3. Проверьте силу теста: временно замените в totals.go += на = — go test . обязан упасть; верните. Потом временно добавьте в цикл пропуск нулей, как в функции выше, — тест снова обязан упасть; верните.

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

▶ Выполните
cd ~/gocourse/day18
git add .
git commit -m "group: тест к totals"

Готово, когда: go test . зелёный на вашей функции и падал на обеих поломках — пункты grouptest и groupzero.


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

Что видите Что это значит Что делать
panic: assignment to entry in nil map map объявлена через var и не создана map[string]int{} или make
invalid map key type []int ключ — срез, а срезы не сравниваются ключ-строка
invalid operation: got != want (map can only be compared to nil) map сравнили через != maps.Equal(got, want) и import "maps"
тест зелёный на функции, которая теряет артикул сравнение got[sku] с нулём не видит отсутствия ключа maps.Equal или len(got)
проверка: «копия не собралась» в grouptest totals объявлена не в totals.go или дважды держать её в totals.go, один раз

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