Функция отдаёт два ответа сразу
В день 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.gopackage 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.gopackage 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