Порядок исполнения: где мы сейчас
До сегодняшнего дня программа читалась сверху вниз: одна строка, потом следующая, циклы возвращались назад. С функциями появляется новое движение — вбок и обратно: выполнение уходит в другое место файла и через некоторое время возвращается ровно туда, откуда ушло.
Управление уходит и возвращается
Ситуация. Понять, в каком порядке выполняются строки, проще всего, расставив печать.
package main
import "fmt"
func boxes(qty int) int {
fmt.Println(" внутри boxes, qty =", qty)
n := qty / 6
fmt.Println(" boxes возвращает", n)
return n
}
func main() {
fmt.Println("до вызова")
total := boxes(12)
fmt.Println("после вызова, total =", total)
}Что увидите.
stagiaire@lab:~/gocourse/day11/scratch$ go run .
до вызова
внутри boxes, qty = 12
boxes возвращает 2
после вызова, total = 2Как это читать. Программа по-прежнему начинается с main — объявление boxes выше по файлу само по себе ничего не выполняет. Дальше:
fmt.Println("до вызова")— печать.- Строка
total := boxes(12). Сначала вычисляется правая часть. Для этого нужно выполнитьboxes, и управление уходит туда. - Параметр
qtyполучает копию аргумента — число 12. - Строки тела
boxesвыполняются по порядку. return nзаканчивает функцию и отдаёт значение назад.- Программа возвращается в ту же строку
total := boxes(12)и доделывает её: присваивает вернувшееся значение вtotal. fmt.Println("после вызова, …")— печать.
Главное здесь — шаги 2 и 6: строка с вызовом посещается дважды. Первый раз — чтобы уйти в функцию, второй — чтобы принять результат. Пока работает boxes, строка total := … не закончена, она ждёт.
Переменные main в это время никуда не деваются: они лежат в своих ячейках и спокойно дождутся возврата. Но изнутри boxes их не видно — функция знает только свои параметры и свои переменные. Это и есть причина ошибки undefined со второй страницы: имя из main в другой функции не существует.
Таблица трассировки с колонкой «где мы»
Таблицу вы ведёте с дня 02: строка на каждую выполненную строку кода, колонка на каждую переменную. Для функций к ней добавляется колонка «где мы» — имя функции, внутри которой сейчас выполняется строка. Без неё номера строк начинают прыгать непонятно куда.
Программа с номерами строк, как в VS Code:
1 package main
2
3 import "fmt"
4
5 func double(n int) int {
6 d := n * 2
7 return d
8 }
9
10 func main() {
11 qty := 3
12 big := double(qty)
13 fmt.Println(big)
14 }| шаг | строка | где мы | qty | big | n | d |
|---|---|---|---|---|---|---|
| 1 | 11 | main | 3 | - | - | - |
| 2 | 12 | main | 3 | - | - | - |
| 3 | 5 | double | 3 | - | 3 | - |
| 4 | 6 | double | 3 | - | 3 | 6 |
| 5 | 7 | double | 3 | - | 3 | 6 |
| 6 | 12 | main | 3 | 6 | - | - |
| 7 | 13 | main | 3 | 6 | - | - |
stagiaire@lab:~/gocourse/day11/scratch$ go run .
6Правила для вызова. Правила 1–6 дня 02, 7–10 дня 06 и 11–15 дня 08 действуют. К ним ещё пять.
- Колонка «где мы» — имя функции, чью строку сейчас выполняем. Пишется словом:
main,double. Пока в таблице стоитmain, работают строкиmain; пока стоит имя функции — её строки. - Строка с вызовом — шаг, и она встречается в таблице дважды. Первый раз — когда управление уходит в функцию: значения на этом шаге ещё старые, результат не получен (шаг 2). Второй раз — когда оно вернулось и присваивание доделано (шаг 6).
- Первый шаг внутри функции — строка её заголовка. На нём параметры получают копии аргументов (шаг 3:
nстал равен 3). Строкаfunc …в таблицу попадает только в этот момент, один раз на вызов. - Строка
return— шаг. Значения на нём те же, что строкой выше:returnничего не меняет, он заканчивает функцию. Следующий шаг — снова строка вызова, вmain. - Переменные другой функции — прочерк. Пока мы в
main, у параметров и переменных функции стоит-: их ещё нет или уже нет. Пока мы внутри функции, переменныеmainпоказываются как есть и не меняются: функция до них не дотягивается.
Самопроверка по таблице: строк с именем функции должно быть столько, сколько строк её тела выполнилось, плюс одна на заголовок; строка вызова встречается парами — уход и возврат.
Открывать три прошлые страницы за правилами 1–15 не нужно: весь сквозной список собран в памятке блока 3 — course page 15 2, раздел «Трассировка: правила 1–24». По нему же сверяется таблица на контрольной точке блока.
Попробуйте сейчас: маленькая таблица на бумаге.
Цель: таблица «шаг | строка | где мы | …» для программы из семи шагов, сверенная с выводом.
1. На бумаге, не запуская, заполните таблицу для программы из раздела выше, но замените
qty := 3наqty := 5. Колонки те же:
шаг | строка | где мы | qty | big | n | d.2. Сколько раз в вашей таблице встретилась строка 12? Совпало ли это с правилом 17?
3. Проверьте запуском — наберите в
scratch/main.goту же программу сqty := 5:✎ Наберите в файлscratch/main.gopackage main import "fmt" func double(n int) int { d := n * 2 return d } func main() { qty := 5 big := double(qty) fmt.Println(big) }▶ Выполнитеcd ~/gocourse/day11/scratch go run .Готово, когда: напечатанное число равно
bigв последней строке вашей таблицы, а строка 12 встретилась ровно дважды.
Функция вызывает функцию
Ситуация. Вес партии — это вес товара плюс вес упаковки, а упаковка считается по числу коробок. Число коробок уже умеет считать boxes, и второй функции незачем повторять этот расчёт: она просто вызывает первую.
package main
import "fmt"
func boxes(qty int) int {
n := qty / 6
if qty%6 != 0 {
n++
}
return n
}
func weight(qty int) int {
return qty*300 + boxes(qty)*120
}
func main() {
fmt.Println("Вес партии:", weight(14), "г")
}stagiaire@lab:~/gocourse/day11/scratch$ go run .
Вес партии: 4560 гКак это читать. Правила те же, просто уходов и возвратов стало два уровня: main уходит в weight, weight — в boxes, boxes возвращается в weight, weight — в main. В колонке «где мы» это выглядит как main, weight, boxes, weight, main — и порядок обязательно симметричный: вернуться можно только туда, откуда пришли.
Глубина вложенности здесь ни на что не влияет. Каждый вызов живёт по одному и тому же правилу: свои параметры, свои переменные, возврат ровно в ту строку, из которой ушли. Именно поэтому программу, разложенную на функции, читают сверху вниз по main и вглубь только там, где это нужно, — об этом будет день 13.
Таблица в файле
Задание trace — в том же формате, что трассировки дней 02, 06 и 08: файл table.txt рядом с program.go, шапка-комментарий
# шаг | строка | где мы | …, по строке на каждую выполненную строку. Шаги, номера строк и прочерки уже вписаны; менять нужно только ?.
В колонке «где мы» пишется имя функции латиницей, без скобок: main или boxes. Регистр букв и число пробелов проверка не смотрит.
Программа в задании вызывает одну и ту же функцию дважды, с разными числами, и внутри неё есть ранний return — значит, два вызова пройдут по разным строкам. Числа у каждого свои: они взяты из вашего зерна. Прочерки в заготовке подскажут, где вы находитесь: у переменных main прочерк стоит до их объявления, у qty и n — во всех строках, когда функция не работает.
Проверка сравнивает номера строк, имена функций, значения и порядок. Где именно ошибка, она не говорит.
Попробуйте сейчас: задание trace.
Цель: все
?вtrace/table.txtзаменены значениями без запуска программы.1. Откройте задание:
▶ Выполнитеcd ~/gocourse/day11/trace cat table.txt2. Откройте в VS Code
program.goиtable.txtрядом и заполните все?не запуская программу. Начните с колонки «где мы»: номера строк уже стоят, по ним сразу видно, в какой функции шаг.3. Потом запустите и сверьте: напечатанное число должно совпасть со значением
totalв последней строке таблицы.▶ Выполнитеgo run .4. Если хочется проверить промежуточные значения — только в копии,
program.goне меняйте:▶ Выполнитеcp program.go ~/gocourse/day11/scratch/main.go cd ~/gocourse/day11/scratch go run .Готово, когда: в
table.txtне осталось?и пунктtraceвcourse check 11зелёный.
Что может пойти не так
| Что видите | Что это значит | Что делать |
|---|---|---|
В таблице ещё осталось знаков ?: 7 |
заполнено не всё | найти ?: в VS Code Ctrl+F |
Строк с шагами в таблице 15, а нужно 16 |
строку удалили или добавили | переименовать table.txt, выполнить course lab 11 — появится новая заготовка |
Все ? заполнены, но где-то значение не совпало |
ошибка в каком-то шаге | пройти вызов заново по правилам 16–20 |
| строка вызова записана один раз | забыт возврат из функции | правило 17: уход и возврат — два разных шага |
| строка заголовка функции пропущена | параметр «появился сам» | правило 18: заголовок — шаг, на нём параметр получает копию |
пока мы внутри функции, переменные main меняются |
функция «дотянулась» до чужих переменных | правило 20: она их не видит; значения переписываются без изменений |
| после возврата у параметра осталось число | параметр живёт только на время работы функции | правило 20: прочерк |
на строке return значение уже новое |
return считают присваиванием |
правило 19: он ничего не меняет, только заканчивает функцию |
Дальше: как называть функции и почему порядок объявления не важен — course next