Как читать программу, разложенную на функции
Программу из одного main читают сверху вниз — это дни 04 и 05. С функциями порядок чтения другой: текст в файле лежит в одном порядке, а выполняется в другом. Этому и учимся: сегодняшний навык нужен каждый день на работе, где кода намного больше, чем помещается в голове.
Два правила чтения
- Начинайте с
mainи читайте его сверху вниз. Что бы ни лежало в файле выше, программа начинается сmain. Функции, объявленные до или после него, — просто описания: они ничего не делают, пока их не позвали. - Вглубь — только когда нужен ответ. Встретили вызов
packCost(...)— сначала спросите себя, достаточно ли вам имени. Если вы читаете программу, чтобы понять, что она делает в целом, имени хватает. Если вам нужно значение или вы ищете ошибку — тогда идёте внутрь функции, дочитываете её доreturnи возвращаетесь в ту же строкуmain, откуда ушли.
Второе из них — то, ради чего функции и нужны. main из шести строк читается как оглавление книги, и вы сами решаете, какую главу открыть.
Ситуация: программа коллеги
В каталоге ~/gocourse/day13/read лежит программа. Надо сказать, что она напечатает, не запуская её.
1 // Программа для чтения, страница 3. Её не надо менять и пока не надо
2 // запускать: сначала ответ на вопрос q2, потом уже go run .
3 package main
4
5 import "fmt"
6
7 func perTrip(total, trips int) int {
8 return (total + trips - 1) / trips
9 }
10
11 func loadPlan(total, trips int) int {
12 load := perTrip(total, trips)
13 if load > 40 {
14 load = 40
15 }
16 return load
17 }
18
19 func main() {
20 total := 150
21 for trips := 2; trips <= 4; trips++ {
22 fmt.Println(trips, loadPlan(total, trips))
23 }
24 }Номера строк слева — как в VS Code: от начала файла, считая комментарии и пустые строки. В самом файле их, конечно, нет.
Обзор по правилу чтения 1. В main три строки: переменная total, цикл по trips от 2 до 4 включительно и печать двух чисел. Значит, вывод — три строки, в каждой два числа. Первое число — trips, второе — результат loadPlan. Что такое loadPlan, пока неважно: чтобы узнать форму вывода, внутрь заходить не нужно.
Вглубь по правилу чтения 2. Второе число нам как раз нужно, поэтому идём внутрь — сначала в loadPlan, из неё в perTrip.
Два правила трассировки к прежним
Правила 1–22 из дней 02, 06, 08, 11 и 12 действуют. Держать в голове пять прошлых страниц не нужно: все правила собраны одним списком в памятке блока 3 — course page 15 2, раздел «Трассировка: правила 1–24». Открывайте его рядом с таблицей; по нему же сверяется таблица на контрольной точке. Сегодня к прежним правилам добавляются два.
23. Одноимённые переменные разных функций — разные колонки. В заголовке пишется, чья переменная: total (main), total (loadPlan). Параметр — копия (день 11), а не та же ячейка, поэтому и колонка своя. Пока в «где мы» стоит main, в колонках loadPlan прочерк, и наоборот — это правило 20 дня 11, просто теперь видно, к какой из двух total он относится.
24. Вложенный вызов раскрывают отдельной таблицей. Строка с вызовом внутри функции по правилу 22 — один шаг: результат уже присвоен, внутрь не идём. Если ответ нужен и оттуда, для этого вызова ведут вторую таблицу, а в первую подставляют её результат — сколько бы уровней ни было.
Трассировка с уходом в функцию
Первый проход цикла, trips равно 2. Идём по правилу чтения 2 — вглубь только там, где нужен ответ, — поэтому вызов perTrip на строке 12 пока один шаг (правило 22).
| шаг | строка | где мы | total (main) | trips (main) | total (loadPlan) | trips (loadPlan) | load |
|---|---|---|---|---|---|---|---|
| 1 | 20 | main |
150 | - | - | - | - |
| 2 | 21 | main |
150 | 2 | - | - | - |
| 3 | 22 | main |
150 | 2 | - | - | - |
| 4 | 11 | loadPlan |
150 | 2 | 150 | 2 | - |
| 5 | 12 | loadPlan |
150 | 2 | 150 | 2 | 75 |
| 6 | 13 | loadPlan |
150 | 2 | 150 | 2 | 75 |
| 7 | 14 | loadPlan |
150 | 2 | 150 | 2 | 40 |
| 8 | 16 | loadPlan |
150 | 2 | 150 | 2 | 40 |
| 9 | 22 | main |
150 | 2 | - | - | - |
Как это читать:
- Шаг 2. Строка 21 — заголовок цикла:
tripsсоздан и равен 2, условие2 <= 4проверено (правила 11 и 12 дня 08). - Шаг 3. Строка 22 — печать, но сначала надо вычислить аргументы и выполнить вызов. Управление уходит в
loadPlan, и строка 22 остаётся недоделанной: в колонкахloadPlanещё прочерки. - Шаг 4. Строка 11 — заголовок функции, правило 18 дня 11. Параметры
totalиtripsполучили копии значений изmain. Имена совпали, а ячейки разные — отсюда четыре колонки вместо двух. - Шаг 5. Строка 12: вызов
perTripпройден одним шагом,loadуже равна 75. Откуда 75 — в следующей таблице. - Шаги 6–7.
75 > 40— истина,loadстановится 40. Строка 15, одинокая}закрывающаяif, шага не даёт. - Шаг 8.
return load— шаг; значение то же, что строкой выше. - Шаг 9. Снова строка 22, второй раз (правило 17): результат 40 встал на место вызова,
Printlnнапечатал2 40. ПеременныхloadPlanбольше нет — прочерки вернулись.
Тот же шаг, но на уровень глубже
Шаг 5 закрыт одной клеткой: load равна 75. Если нужно не поверить, а проверить, для этого вызова ведут вторую таблицу — правило 24. Колонок в ней всего две: у perTrip только свои параметры.
| шаг | строка | где мы | total (perTrip) | trips (perTrip) |
|---|---|---|---|---|
| 5.1 | 12 | loadPlan |
- | - |
| 5.2 | 7 | perTrip |
150 | 2 |
| 5.3 | 8 | perTrip |
150 | 2 |
| 5.4 | 12 | loadPlan |
- | - |
Шаги 5.1 и 5.4 — та же строка 12, уход и возврат. На шаге 5.2 параметры получили копии, на 5.3 считается (150 + 2 - 1) / 2, то есть 151 / 2, то есть 75: деление целых отбрасывает дробь (день 03). Результат 75 уезжает в клетку load шага 5 первой таблицы — и вторая таблица больше не нужна. Так и работают вместе оба правила чтения: таблица на одном уровне, а вглубь — ровно там, где число вызывает сомнение.
Дальше цикл повторяется для trips = 3 и trips = 4. Числа считаются так же: (150 + 3 - 1) / 3 и (150 + 4 - 1) / 4.
Попробуйте сейчас: читаем program.go по вызовам.
Цель: таблица второго прохода заполнена, третья строка вывода предсказана до запуска.
1. Откройте программу и заготовку таблицы:
▶ Выполнитеcd ~/gocourse/day13/read cat program.go code ~/gocourse/day13/read/table.txt2. Заполните
table.txt: это второй проход цикла,tripsравно 3. Шаги и номера строк уже стоят — меняйте только знаки?: имя функции в колонке «где мы» и значения переменных после того, как строка выполнилась. Прочерки оставьте как есть, строк не добавляйте. ВызовperTripна строке 12 — один шаг (правило 22); если его число вызывает сомнение, распишите этот вызов отдельной таблицей на бумаге (правило 24).3. Тем же порядком пройдите третий проход,
tripsравно 4, — уже на бумаге. Выпишите, что напечатает третья строка вывода: два числа через пробел.4. Сдайте их:
▶ Выполните · выделенное замените своимcourse answer day13.q2 ЧИСЛО ЧИСЛОВместо
ЧИСЛО ЧИСЛО— третья строка вывода целиком, два числа через пробел.5. Только теперь запустите и сверьте все три строки:
▶ Выполнитеgo run .Готово, когда: пункты
traceиq2вcourse check 13зелёные, а третья строка запуска совпала с отправленной. Не совпало — найдите шаг, на котором ваша таблица разошлась с программой, и отправьте ответ заново.
Переход к объявлению: F12
В файле на двадцать строк функцию находят глазами. В рабочем сервисе функций тысячи, и они лежат в разных файлах. Поэтому редактор умеет прыгать к объявлению.
Откройте папку дня в VS Code (code ~/gocourse/day13), а в ней read/program.go. Поставьте курсор на имя loadPlan в строке 22 — там, где эта функция вызывается:
| Что нажать | Что произойдёт |
|---|---|
F12 или Ctrl+клик |
курсор прыгает к строке 11 — туда, где loadPlan объявлена |
Alt+← |
вернуться назад, туда, откуда прыгнули |
Shift+F12 |
показать все места, где эта функция вызывается |
| наведение мыши на имя | всплывающая подсказка с заголовком функции |
Ровно так читают чужой код: F12 — вглубь, Alt+← — обратно. Это и есть правило чтения 2, только руками редактора.
Полный список клавиш и настройка редактора — в гайде по рабочему месту: docs/setup/ide.md, раздел «Клавиши, которые понадобятся с первого дня».
Если F12 никуда не прыгает, значит, не работает языковой сервер gopls — редактор просто не знает, где что объявлено. Что делать, написано там же, в разделе «Если что-то не так».
Дерево вызовов
Когда функций становится больше трёх, держать в голове, кто кого зовёт, уже не выходит. Помогает набросок на бумаге — дерево вызовов: от main вниз, по одной ветке на вызов.
Возьмём программу приёмки со страницы 1 и добавим к ней одну связь: представим, что packCost больше не получает порог скидки аргументом, а спрашивает у bulkDelivery со страницы 2, крупная ли отгрузка. Тогда дерево выглядит так:
main
├─ boxesFor
├─ packCost
│ └─ bulkDelivery
└─ rublesЧитается сверху вниз и слева направо: main зовёт три функции, а одна из них, packCost, внутри зовёт четвёртую. Дерево строится за две минуты и отвечает сразу на несколько вопросов:
- Где искать ошибку в числе. Неверная стоимость — смотреть
packCostи всё, что под ней;rublesиboxesForтут ни при чём. - Что сломается, если поменять функцию. Меняете
bulkDelivery— меняется иpackCost, и всё, что зовёт её. Вверх по дереву. - Что можно читать по отдельности. Ветки, которые не пересекаются, друг на друга не влияют.
Листья дерева — функции, которые никого не зовут (boxesFor, rubles, bulkDelivery). Их проще всего и читать, и проверять: результат зависит только от аргументов. С них и начинают, когда разбираются в незнакомом коде снизу вверх.
То же дерево редактор умеет показывать сам: Shift+F12 на имени функции даёт список мест, откуда её зовут, — это ветка вверх. F12 на вызове — ветка вниз.
Как этим деревом сужают поиск ошибки — в дополнительном чтении дня (course extra 13), раздел «Где искать ошибку: сужаем по дереву вызовов». Приём тот же, что деление поиска пополам в дне 09, только делятся не строки, а функции.
Что кажется и как на самом деле
| Что кажется | Как на самом деле |
|---|---|
| функции выполняются в том порядке, в каком написаны в файле | выполняются в порядке вызовов; порядок объявления в файле не важен вовсе |
раз perTrip объявлена первой, она и выполнится первой |
она выполнится, когда её позовут — внутри loadPlan |
переменная load останется после возврата |
она исчезает вместе с вызовом |
total внутри loadPlan — та же переменная, что в main |
это её копия; имена совпали случайно |
| чтобы понять программу, надо прочитать все функции | достаточно main, пока не нужен конкретный ответ |
| печать происходит там, где посчитано значение | печатает только тот, в чьём коде стоит Println |
Что может пойти не так
| Что видите | Что это значит | Что делать |
|---|---|---|
| предсказание не сошлось на одном числе | ошибка внутри одного вызова | расписать этот вызов отдельной таблицей — правило 24 |
| потеряли, куда возвращаться после вызова | читаете вглубь без пометки | отмечать строку возврата: в редакторе это Alt+← |
F12 не работает |
нет gopls или папка открыта не как корень модуля |
открыть именно ~/gocourse/day13, проверка — docs/setup/ide.md |
| читаете функцию, а она никем не вызвана | мёртвый код или опечатка в имени при вызове | Shift+F12: если вызовов нет, функция не работает |
| в двух функциях переменные с одним именем — путаница | это разные переменные | колонка на каждую, в заголовке имя функции — правило 23 |
| в таблице строка вызова записана один раз | забыт возврат | уход и возврат — два шага, правило 17 дня 11 |
| строка заголовка функции пропущена | параметры «появились сами» | заголовок — шаг, на нём параметры получают копии, правило 18 |
| после возврата в колонках функции остались числа | её переменные «доживают» до следующих шагов | они исчезают вместе с вызовом: прочерк, правило 20 |
Дальше: как функции получают данные — course next