День 11 · Своя функция: объявление, параметры, возврат · страница 4 из 7

Порядок исполнения: где мы сейчас

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


Управление уходит и возвращается

Ситуация. Понять, в каком порядке выполняются строки, проще всего, расставив печать.

Код для чтения · разбираем, набирать не нужно
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 выше по файлу само по себе ничего не выполняет. Дальше:

  1. fmt.Println("до вызова") — печать.
  2. Строка total := boxes(12). Сначала вычисляется правая часть. Для этого нужно выполнить boxes, и управление уходит туда.
  3. Параметр qty получает копию аргумента — число 12.
  4. Строки тела boxes выполняются по порядку.
  5. return n заканчивает функцию и отдаёт значение назад.
  6. Программа возвращается в ту же строку total := boxes(12) и доделывает её: присваивает вернувшееся значение в total.
  7. 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 действуют. К ним ещё пять.

  1. Колонка «где мы» — имя функции, чью строку сейчас выполняем. Пишется словом: main, double. Пока в таблице стоит main, работают строки main; пока стоит имя функции — её строки.
  2. Строка с вызовом — шаг, и она встречается в таблице дважды. Первый раз — когда управление уходит в функцию: значения на этом шаге ещё старые, результат не получен (шаг 2). Второй раз — когда оно вернулось и присваивание доделано (шаг 6).
  3. Первый шаг внутри функции — строка её заголовка. На нём параметры получают копии аргументов (шаг 3: n стал равен 3). Строка func … в таблицу попадает только в этот момент, один раз на вызов.
  4. Строка return — шаг. Значения на нём те же, что строкой выше: return ничего не меняет, он заканчивает функцию. Следующий шаг — снова строка вызова, в main.
  5. Переменные другой функции — прочерк. Пока мы в 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.go
package 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.txt

2. Откройте в 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