День 12 · Несколько результатов и ошибка как результат · страница 1 из 7

Функция отдаёт два ответа сразу

В день 11 функция возвращала одно значение — и этого хватало, пока ответ был один. Но вопросы к складу редко бывают одиночными: «сколько полных коробок» почти всегда идёт вместе с «а сколько штук останется россыпью». Считаются эти два числа из одних и тех же данных, одним и тем же делением. Разносить их по двум функциям — значит два раза передавать одно и то же и два раза делить. В Go функция может вернуть сразу несколько значений.


Два результата в заголовке

Ситуация. Партия 17 штук, коробка вмещает 5. Нужно знать и число полных коробок, и остаток.

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

import "fmt"

func pack(qty int, size int) (int, int) {
	boxes := qty / size
	rest := qty % size
	return boxes, rest
}

func main() {
	boxes, rest := pack(17, 5)
	fmt.Println("коробок:", boxes, "в последней:", rest)
}

Что увидите.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day12/scratch$ go run .
коробок: 3 в последней: 2

Как это читать. Изменились три места.

Место Как было при одном результате Как при двух
заголовок функции func pack(qty int, size int) int func pack(qty int, size int) (int, int)
return return boxes return boxes, rest
вызов boxes := pack(17, 5) boxes, rest := pack(17, 5)

Типы результатов перечисляются в круглых скобках через запятую. Скобки при двух и более результатах обязательны: func pack(...) int, int компилятор не поймёт. В return значения идут через запятую в том же порядке, что и типы в заголовке. Слева от := — столько переменных, сколько результатов, и снова в том же порядке.

Имена переменных на стороне вызова свои: функция называет свои значения boxes и rest, но вызывающий может назвать их full и tail — значения встанут по порядку, а не по имени.

Сколько результатов бывает? Технически сколько угодно, на практике — два, редко три. Четыре результата обычно значат, что функция делает несколько дел сразу, и её пора разделить.


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

Цель: программа в scratch считает коробки и остаток одной функцией и печатает оба числа.

1. Сотрите заготовку в scratch/main.go и наберите руками программу из раздела «Два результата в заголовке»:

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

import "fmt"

func pack(qty int, size int) (int, int) {
	boxes := qty / size
	rest := qty % size
	return boxes, rest
}

func main() {
	boxes, rest := pack(17, 5)
	fmt.Println("коробок:", boxes, "в последней:", rest)
}

Запустите:

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

2. Поменяйте вызов на pack(40, 12) и запустите снова. Посчитайте на бумаге заранее: сколько полных коробок и сколько останется.

3. Поменяйте местами переменные слева: rest, boxes := pack(40, 12). Числа в печати те же, а смысл другой — Go кладёт значения по порядку, имена ему ничего не говорят.

Готово, когда: на pack(40, 12) программа печатает коробок: 3 и в последней: 4, а после перестановки переменных вы видите, что числа в строке поменялись местами.


Ненужный результат: _

Иногда из двух ответов нужен один. Оставить переменную «про запас» нельзя: правило «объявил — используй» никуда не делось, и компилятор скажет declared and not used. Для такого случая есть знак подчёркивания _ — вы его видели в дне 07, когда разбирали, почему нельзя выбрасывать ошибку.

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

import "fmt"

func pack(qty int, size int) (int, int) {
	return qty / size, qty % size
}

func main() {
	boxes, _ := pack(17, 5)
	fmt.Println("коробок:", boxes)
	_, rest := pack(17, 5)
	fmt.Println("в последней:", rest)
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day12/scratch$ go run .
коробок: 3
в последней: 2

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

Код для чтения · разбираем, набирать не нужно
	boxes, _ := pack(17, 5)
	fmt.Println(boxes, _)
Вывод · это печатает программа, набирать не нужно
# day12/scratch
./main.go:11:21: cannot use _ as value or type

Ещё одна деталь из этого примера: return qty / size, qty % size — считать прямо в return можно, промежуточные переменные нужны только для читаемости.

Правило дня 07 остаётся в силе: _ на месте ошибки не пишем. Выбрасывать можно результат, который вам действительно не нужен, а не тот, который не хочется проверять.


Порядок результатов

Ситуация. Программа собралась, работает, но печатает не те числа. Ошибка — в одной строке.

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

import "fmt"

func pack(qty int, size int) (int, int) {
	return qty / size, qty % size
}

func main() {
	boxes, rest := pack(20, 6)
	rest, boxes = pack(9, 4)
	fmt.Println(boxes, rest)
}

Во второй строке main переменные слева стоят в обратном порядке, а = вместо := потому, что обе уже объявлены. Компилятор не возражает: типы совпадают, оба результата — целые числа. Что попадёт в boxes, а что в rest, решает позиция, и никто не подскажет, что смысл разъехался.

Поэтому у функции с несколькими результатами порядок описывают словами — в комментарии или в имени: pack возвращает «сколько коробок и сколько осталось», именно в таком порядке.


Попробуйте сейчас: порядок результатов.

Цель: предсказать два числа, которые напечатает программа выше, и сдать их.

1. Не запуская, выпишите на бумаге: что вернёт pack(20, 6), что вернёт pack(9, 4), что после этого лежит в boxes и что в rest.

2. Сдайте два числа через пробел — в том порядке, в каком их напечатает fmt.Println(boxes, rest):

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

3. Потом проверьте запуском — наберите ту же программу в scratch/main.go:

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

import "fmt"

func pack(qty int, size int) (int, int) {
	return qty / size, qty % size
}

func main() {
	boxes, rest := pack(20, 6)
	rest, boxes = pack(9, 4)
	fmt.Println(boxes, rest)
}
▶ Выполните
cd ~/gocourse/day12/scratch
go run .

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


Именованные результаты — чтобы узнать в чужом коде

Имена результатов можно написать и в своей функции:

Код для чтения · разбираем, набирать не нужно
func pack(qty int, size int) (boxes int, rest int) {
	boxes = qty / size
	rest = qty % size
	return boxes, rest
}

Переменные boxes и rest здесь объявляет сам заголовок, поэтому внутри идёт =, а не :=. Работает это ровно так же: печатает 3 2. В курсе мы так писать не будем — обычных результатов достаточно, — но в чужом коде такой заголовок встречается, и теперь он не удивит.


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

Сообщения ниже сняты на Go 1.27.1: это то, что вы увидите буква в букву.

Код для чтения · разбираем, набирать не нужно
func pack(qty int, size int) (int, int) {
	return qty / size
}
Вывод · это печатает программа, набирать не нужно
# day12/scratch
./main.go:6:9: not enough return values
	have (int)
	want (int, int)

have — что написано в return, want — что обещано в заголовке. Читаются эти две строки так же, как have/want у аргументов в дне 04.

Что видите Что это значит Что делать
not enough return values, ниже have (int) и want (int, int) в return значений меньше, чем в заголовке дописать недостающее значение через запятую
too many return values, ниже have (int, int) и want (int) в return значений больше, чем в заголовке убрать лишнее или добавить тип в заголовок
./main.go:9:1: missing return из функции есть путь, на котором return не встретился (например, return только внутри if) return в конце функции
assignment mismatch: 1 variable but pack returns 2 values слева одна переменная, а результата два boxes, rest := pack(…) или boxes, _ := pack(…)
assignment mismatch: 2 variables but boxes returns 1 value слева две переменные, а результат один посмотреть заголовок функции: сколько она отдаёт
multiple-value pack(17, 5) (value of type (int, int)) in single-value context вызов с двумя результатами подставлен рядом с другими аргументами: fmt.Println("итого:", pack(17, 5)) сначала в переменные, потом печать
cannot use _ as value or type из _ пытаются прочитать значение _ только слева от := или =
undefined: rest переменной нет: слева от := её забыли или там стоит _ объявить её в том же :=, где вызов

Отдельно про fmt.Println(pack(17, 5)): это собирается и печатает 3 2. Вызов с несколькими результатами можно подставить в другой вызов, только если он там единственный аргумент. Как только рядом появляется ещё что-нибудь, получается multiple-value … in single-value context.


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

Цель: в ~/gocourse/day12 есть репозиторий с первым коммитом — заготовками дня. Каждое задание дальше заканчивается своим коммитом.

▶ Выполните
cd ~/gocourse/day12
git init
git add .
git commit -m "день 12: заготовки"
git log --oneline

Готово, когда: git log --oneline печатает одну строку с вашим сообщением. Если git просит подпись автора — git config --global user.name и user.email, как в дне 11. Подробнее про коммиты — седьмая страница.


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

Цель: fix2 собирается и печатает две строки — партию и разбор по коробкам.

1. Программа fix2 не собирается. Посмотрите на все сообщения:

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

Сообщений три. Первое — про функцию pack, два других — следствия: вызов и печать уже написаны в расчёте на два результата.

2. Почините, начав с первого сообщения. Заголовок pack и числа в main менять нельзя, правка — одна-две строки, больше двух проверка не примет (строки import не считаются). После правки запустите снова.

3. Числа у каждого свои: они записаны прямо в вашем main.go.

4. Починка закончена — закоммитьте её:

▶ Выполните
cd ~/gocourse/day12
git add .
git commit -m "fix2: два результата у pack"

Готово, когда: go run . печатает две строки — Партия: … шт. и полных коробок: … россыпью: …, а в course check 12 зелёные пункты fix2 и (после второй починки на странице 5) fixsmall.


Дальше: где живёт переменная — блок, функция, программа — course next