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 нет». Таблицы, куда писать, не существует.
- Чтение из
nil-map работает как из пустой: длина 0, любой ключ даёт ноль. Поэтому ошибку не видно, пока программа только читает. - Запись — паника
assignment to entry in nil map, строка 8. Разбор паники — как в дне 03;exit status 2дописалgo run. delete,rangeиPrintlnпоnil-map ведут себя как с пустой. Отличить можно толькоcounts == nil.
Лечится созданием map до первой записи: counts := map[string]int{} или make(map[string]int).
Попробуйте сейчас: починить fix1.
Цель: программа в
~/gocourse/day18/fix1перестаёт падать и считает коробки.1. Прочитайте комментарий в начале
main.goи запустите:▶ Выполнитеcd ~/gocourse/day18/fix1 go run . < example.txt2. По номеру строки из паники найдите запись в 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.gopackage 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.gopackage 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