День 16 · Срезы: много значений в одной переменной · страница 6 из 7

Срез в функции и тест

Со срезами функции и тесты пишутся как в блоке 3. Два отличия покажут опыты этой страницы.


Срез — параметр функции

Ситуация. Сумма остатков нужна в нескольких местах — пусть её считает функция.

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

import "fmt"

func total(stock []int) int {
	sum := 0
	for _, qty := range stock {
		sum += qty
	}
	return sum
}

func main() {
	stock := []int{12, 40, 5}
	fmt.Println(total(stock))
	fmt.Println(total([]int{7, 3}))
	fmt.Println(total([]int{}))
}

Что увидите.

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

Как это читать. Тип параметра — []int, без длины: функция принимает срез любой длины, хоть пустой. Аргументом можно передать переменную или сразу литерал []int{7, 3}. Внутри функции со срезом работают как обычно: range, len, индекс.


Попробуйте сейчас: что выведет функция со срезом.

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

1. Не запуская, прочитайте программу. Функция получает срез и число и меняет внутри оба:

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

import "fmt"

func reserve(stock []int, n int) {
	stock[0] = stock[0] - n
	n = 0
}

func main() {
	stock := []int{12, 40}
	n := 5
	reserve(stock, n)
	fmt.Println(stock[0], n)
}

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

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

3. Проверьте запуском — программа целиком в черновик:

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

import "fmt"

func reserve(stock []int, n int) {
	stock[0] = stock[0] - n
	n = 0
}

func main() {
	stock := []int{12, 40}
	n := 5
	reserve(stock, n)
	fmt.Println(stock[0], n)
}
▶ Выполните
cd ~/gocourse/day16/scratch
go run .

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


Разбор. Число n пришло в функцию копией — день 11: n = 0 меняет копию, в main осталось 5. А запись по индексу в срез-параметр видна снаружи: stock[0] в main стал 7.

Сегодня это факт: элементы среза, изменённые внутри функции, меняются и у вызвавшего. В функцию копируется окно, а ряд ячеек под ним общий; картинку разберём в день 17 — там же увидим, почему append внутри функции снаружи не виден.


Функция, которая возвращает срез

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

Код для чтения · разбираем, набирать не нужно
func toOrder(stock []int, full int) []int {
	order := []int{}
	for _, qty := range stock {
		need := 0
		if qty < full {
			need = full - qty
		}
		order = append(order, need)
	}
	return order
}

Для toOrder([]int{12, 40, 5}, 30) она возвращает [18 0 25]. Тест к ней по образцу дня 14 напрашивается сам — и не собирается:

Код для чтения · разбираем, набирать не нужно
	got := toOrder([]int{12, 40, 5}, 30)
	want := []int{18, 0, 25}
	if got != want {
		t.Errorf("toOrder = %v, хотели %v", got, want)
	}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day16/scratch$ go test .
# day16/scratch [day16/scratch.test]
./order_test.go:8:5: invalid operation: got != want (slice can only be compared to nil)
FAIL	day16/scratch [build failed]
FAIL

Срезы в Go знаком == и != не сравниваются: разрешено только сравнение с nil — особым значением: срез без массива называют nil-срезом, о нём в день 17. Сравнивать приходится самим — как человек сверяет две описи: сначала число строк, потом строку за строкой.

Код для чтения · разбираем, набирать не нужно
func TestToOrder(t *testing.T) {
	got := toOrder([]int{12, 40, 5}, 30)
	want := []int{18, 0, 25}
	if len(got) != len(want) {
		t.Fatalf("toOrder: длина %d, хотели %d: %v", len(got), len(want), got)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Errorf("toOrder: ячейка %d: %d, хотели %d", i+1, got[i], want[i])
		}
	}
}

Как это читать. Длина — первой и через t.Fatalf из дня 14: если длины разные, цикл по want спросил бы у got индекс, которого нет, и тест упал бы паникой. Потом элементы сравниваются попарно; %v печатает срез целиком.

Что увидите, если в функции убрать if и считать need := full - qty всегда:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day16/scratch$ go test .
--- FAIL: TestToOrder (0.00s)
    order_test.go:13: toOrder: ячейка 2: -10, хотели 0
FAIL
FAIL	day16/scratch	0.009s
FAIL

А если цикл в функции начать с индекса 1, пропадёт первая ячейка — и сработает проверка длины:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day16/scratch$ go test .
--- FAIL: TestToOrder (0.00s)
    order_test.go:9: toOrder: длина 2, хотели 3: [0 25]
FAIL
FAIL	day16/scratch	0.010s
FAIL

Таблица случаев — это срез

Теперь можно объяснить форму, которую в дне 14 набирали по образцу:

Код для чтения · разбираем, набирать не нужно
	cases := []struct {
		name  string
		stock []int
		limit int
		want  []int
	}{
		{"две ниже", []int{12, 3, 40, 5}, 10, []int{2, 4}},
		{"никого ниже", []int{10, 20}, 10, []int{}},
	}
	for _, c := range cases {

[]…{…} — срез, как []int{12, 40}: в фигурных скобках его элементы, по одному на строку. Элемент здесь не число, а запись из нескольких полей — что такое struct и как описывают поля, разберём в блоке 5. for _, c := range cases — обычный обход среза с этой страницы: c — очередной случай. Поле случая берут через точку: c.stock, c.want; подробнее — блок 5. Полями случая могут быть и срезы: stock []int, want []int.


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

Цель: low_test.go в ~/gocourse/day16/low ловит обе подмены функции lowCells.

1. Прочитайте условие и функцию:

▶ Выполните
cd ~/gocourse/day16/low
cat TASK.txt
cat low.go
go run .

2. Напишите low_test.go: сравнение длины и каждого элемента; среди случаев — такой, где ниже порога последняя ячейка. Можно одним тестом с таблицей, можно несколькими тестовыми функциями.

3. Запустите:

▶ Выполните
go test .
go test -v .
gofmt -l .

4. Проверьте силу теста: временно замените в low.go строку low = append(low, i+1) на low = append(low, i) — тест обязан упасть. Верните файл из оригинала и прогоните тест ещё раз:

▶ Выполните
go test .
cp ~/.course/orig/day16/low/low.go low.go
go test .

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

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

Готово, когда: go test . печатает ok на верной функции и FAIL на подменённой — пункты lowtest и lowedge.


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

Что видите Что это значит Что делать
invalid operation: got != want (slice can only be compared to nil) срезы сравнивают знаком длина, потом цикл по элементам
lowtest красный: тест проходит на подмене сравнивается только длина, а номера — нет после проверки длины — сравнение got[i] и want[i]
lowedge красный ни в одном случае ниже порога не оказалась последняя ячейка добавьте случай, где последний элемент меньше порога
изменили срез внутри функции, и он «испортился» у вызвавшего запись по индексу в срез-параметр видна снаружи так устроен срез; как сделать копию — день 17
две строки lowCells redeclared in this block и other declaration of lowCells, потом FAIL day16/low [build failed] функцию объявили ещё раз в файле теста в тесте функцию только вызывают, объявление остаётся в low.go

Дальше: ритуал перед коммитом и словарик — course next