Как читать чужую программу
Четыре задачи контрольной точки — не на написание, а на чтение: починить программу, которая не собирается; починить ту, что считает неверно; заполнить таблицу трассировки; назвать значения переменных, которых программа не печатает. На работе так же: прежде чем поменять строку в чужом сервисе, её надо прочитать и понять, что она делает сейчас.
Компьютер не читает «по смыслу»
Человек читает программу как текст: пробегает глазами и угадывает замысел по именам. Компьютер выполняет одну строку за другой и помнит только текущие значения переменных. Почти любая ошибка чтения — подмена одного другим: «переменная называется free, значит, в ней свободное место». А в ней то, что было присвоено последним.
Правила из дней 01–04, на которых держится чтение:
| Правило | Что из него следует |
|---|---|
тело main выполняется сверху вниз, строка за строкой |
печать происходит там, где стоит Println, — не раньше и не позже |
| в переменной — последнее присвоенное значение | чтобы узнать значение, ищите последнее присваивание выше текущей строки |
| присваивание копирует значение в момент выполнения | free := stock - reserved — не формула: изменится stock — free останется прежней |
| правая часть считается целиком до присваивания | x = x + 1 берёт старое x; a, b = b, a меняет значения местами |
| целые делятся с отбрасыванием дроби, порядок действий — как в математике | 50/12*12 — это 48, а не 50 |
| функция возвращает результат, а не меняет свой аргумент | strings.ToUpper(name) без присваивания ничего не делает с name |
Порядок чтения
- Обзор. Какие пакеты подключены, какие переменные есть, где печать.
- Трассировка. Таблица: после каждой строки — значения всех переменных.
- Предсказание. Записать, что программа напечатает. Именно записать: ответ «в уме» после запуска незаметно подстраивается под увиденное.
- Запуск.
- Разбор расхождения. Не совпало — найти шаг таблицы, где ошиблись, и понять, какое правило сработало не так, как вы думали: расхождение показывает, что именно понято неверно.
Разбор: остатки на складе
Ситуация. Коллега написал программу и говорит, что она печатает свободный остаток. Прочитаем её, прежде чем верить.
package main
import (
"fmt"
"strings"
)
func main() {
stock := 40
reserved := 12
free := stock - reserved
stock = stock + 25
reserved += free / 4
title := strings.ToUpper("остатки")
fmt.Println(title)
fmt.Printf("На складе %d, в резерве %d, свободно %d\n", stock, reserved, free)
}Строки считаем от начала файла, как их нумерует VS Code слева от кода, — комментарии и пустые строки тоже считаются. Этот файл начинается с package main: stock := 40 — девятая строка, fmt.Printf — шестнадцатая. Во многих файлах контрольной первой строкой стоит комментарий, и номера там сдвинуты; в заготовках таблиц это уже учтено.
Трассировка. Правила — те же, что в день 02, стр. 4: строка таблицы на каждую выполненную строку main, печать тоже; прочерк — переменной ещё нет.
| шаг | строка | stock | reserved | free | title |
|---|---|---|---|---|---|
| 1 | 9 | 40 | - | - | - |
| 2 | 10 | 40 | 12 | - | - |
| 3 | 11 | 40 | 12 | 28 | - |
| 4 | 12 | 65 | 12 | 28 | - |
| 5 | 13 | 65 | 19 | 28 | - |
| 6 | 14 | 65 | 19 | 28 | ОСТАТКИ |
| 7 | 15 | 65 | 19 | 28 | ОСТАТКИ |
| 8 | 16 | 65 | 19 | 28 | ОСТАТКИ |
Шаг 5: free / 4 — это 28 ÷ 4 = 7, и reserved становится 12 + 7 = 19. Шаги 7 и 8 — строки с печатью: значения не меняются, их переписывают из строки выше.
Предсказание. По таблице: ОСТАТКИ и На складе 65, в резерве 19, свободно 28.
Что увидите.
stagiaire@lab:~/gocourse/day05/scratch$ go run .
ОСТАТКИ
На складе 65, в резерве 19, свободно 28Как это читать. По смыслу хочется ответить «свободно 46»: товара стало 65, в резерве 19. Но строки, которая пересчитала бы free после этих изменений, в программе нет. free получила значение на шаге 3 и больше не менялась. Если программа должна печатать 46 — это ошибка в программе, и чинится она строкой free = stock - reserved перед печатью. Ошибки такого рода — «собирается, но считает не то» — предмет задачи 5 контрольной точки.
Попробуйте сейчас: программа «остатки» в черновике.
Цель: получить вывод из блока «Что увидите», а после новой строки предсказать вывод по таблице до запуска.
- Черновик
~/gocourse/day05/scratch/main.goуже создан стендом. Замените всё, что в нём лежит, на эту программу — ту же, что разобрана выше:✎ Наберите в файлscratch/main.gopackage main import ( "fmt" "strings" ) func main() { stock := 40 reserved := 12 free := stock - reserved stock = stock + 25 reserved += free / 4 title := strings.ToUpper("остатки") fmt.Println(title) fmt.Printf("На складе %d, в резерве %d, свободно %d\n", stock, reserved, free) }
- Запустите — вывод должен совпасть с блоком «Что увидите»:
▶ Выполнитеcd ~/gocourse/day05/scratch go run .
- Теперь добавьте одну строку —
free = stock - reservedсразу послеstock = stock + 25. Целиком программа станет такой:✎ Наберите в файлscratch/main.gopackage main import ( "fmt" "strings" ) func main() { stock := 40 reserved := 12 free := stock - reserved stock = stock + 25 free = stock - reserved reserved += free / 4 title := strings.ToUpper("остатки") fmt.Println(title) fmt.Printf("На складе %d, в резерве %d, свободно %d\n", stock, reserved, free) }
- До запуска составьте на бумаге новую таблицу трассировки и запишите все три числа, которые программа напечатает. Потом
go run .и сверьте: почему несвободно 46?Готово, когда: первый запуск совпал с «Что увидите», три числа второго запуска совпали с записанными до него, и вы можете объяснить по таблице, почему не
свободно 46.
Таблица трассировки в файле
На контрольной точке таблица — текстовый файл table.txt в формате дня 02. Та же трассировка выглядела бы так:
# шаг | строка | stock | reserved | free | title
1 | 9 | 40 | - | - | -
2 | 10 | 40 | 12 | - | -
3 | 11 | 40 | 12 | 28 | -
4 | 12 | 65 | 12 | 28 | -
5 | 13 | 65 | 19 | 28 | -
6 | 14 | 65 | 19 | 28 | ОСТАТКИ
7 | 15 | 65 | 19 | 28 | ОСТАТКИ
8 | 16 | 65 | 19 | 28 | ОСТАТКИПервая строка — шапка-комментарий: она начинается с #, и проверка её не читает. В заготовке шаги, номера строк и прочерки уже стоят, вместо
значений — знаки ?; менять можно только их. Колонки разделены знаком |, пробелы вокруг него и регистр букв не важны. Проверка сверяет таблицу целиком: одна неверная клетка — таблица не сошлась.
Программа, которая не собирается
Порядок — из дня 01: сообщение целиком → строка и колонка → сравнить с образцом (четвёртая страница — памятка) → одна правка → запуск. Главное на контрольной — чинить, а не переписывать: менять только строки, в которых ошибка.
Одна причина нередко даёт несколько сообщений:
func main() {
total := 450
fmt.Println(totl)
}stagiaire@lab:~/gocourse/day05/scratch$ go run .
# day05/scratch
./main.go:6:2: declared and not used: total
./main.go:7:14: undefined: totlСообщений два, опечатка одна: имени totl нет, и из-за этого total нигде не используется. Правка — одна буква в седьмой строке, и оба сообщения исчезнут. Прежде чем править, прочитайте все сообщения и найдите общую причину.
Программа, которая считает не то
Компилятор молчит, программа работает, числа неверные. Порядок:
- Сравнить вывод с ожидаемым построчно и найти первое неверное число.
- Найти строку, которая его печатает, и переменную в ней.
- Проследить только эту переменную вверх по программе: каждое её присваивание — строка таблицы.
- Найти шаг, где значение разошлось с тем, что должно быть.
Самое коварное — ошибки, о которых молчат и компилятор, и go vet:
name := "кабель"
strings.ToUpper(name)
fmt.Println(name)stagiaire@lab:~/gocourse/day05/scratch$ go run .
кабель
stagiaire@lab:~/gocourse/day05/scratch$ go vet .
stagiaire@lab:~/gocourse/day05/scratch$strings.ToUpper вернула КАБЕЛЬ, но результат никуда не записан и потерян. Нужно name = strings.ToUpper(name).
Что может пойти не так
| Что видите | Что это значит | Что делать |
|---|---|---|
| предсказали «свободно 46», программа печатает 28 | переменную «пересчитали» в уме после изменения других | в таблице значение меняется только на строке с присваиванием этой переменной |
обмен a, b = b, a в таблице дал два одинаковых значения |
обмен посчитан как два присваивания по очереди | правая часть считается целиком до присваивания |
50/12*12 посчитали как 50 |
деление целых отбрасывает дробь уже на первом действии | считать слева направо: 50/12 = 4, 4*12 = 48 |
| «запустил, посмотрел — да, так и думал» | предсказания не было, запуск подменил его | записывать ответ до запуска |
| починили одну ошибку, появилась другая строка с ошибкой | компилятор показал следующую | так бывает; читать и чинить по одной |
diff показывает изменения в строках, которые не трогали |
редактор переформатировал файл, или случайная правка | вернуть оригинал и чинить заново точечно |
Попробуйте сейчас: что выведет, не запуская.
Цель: по таблице трассировки предсказать строку вывода и отправить её.
Прочитайте программу, составьте таблицу трассировки на бумаге и только потом отвечайте.
Код для чтения · разбираем, набирать не нужноpackage main import "fmt" func main() { price := 1999 qty := 3 total := price * qty price = 2499 discount := total / 100 * 5 total -= discount fmt.Printf("%d.%02d\n", total/100, total%100) }Отправьте, что программа напечатает:
▶ Выполните · выделенное замените своимcourse answer day05.q1 ВАШ_ОТВЕТВместо
ВАШ_ОТВЕТ— строка, которую, по вашей таблице, напечатает программа.После ответа сверьтесь запуском. Теперь ту же программу нужно набрать — в
scratch/main.go:✎ Наберите в файлscratch/main.gopackage main import "fmt" func main() { price := 1999 qty := 3 total := price * qty price = 2499 discount := total / 100 * 5 total -= discount fmt.Printf("%d.%02d\n", total/100, total%100) }▶ Выполнитеcd ~/gocourse/day05/scratch go run .Не совпало — найдите шаг таблицы, где разошлись, и отправьте ответ заново — засчитывается последний.
Готово, когда:
course answerответилПринято: day05.q1 = …, а выводgo run .вscratchсовпал с отправленной строкой (пункт проверкиq1).
Дальше: словарик ошибок и памятка-справочник по блоку — course next