День 05 · Контрольная точка 1: как решать задачу · страница 3 из 5

Как читать чужую программу

Четыре задачи контрольной точки — не на написание, а на чтение: починить программу, которая не собирается; починить ту, что считает неверно; заполнить таблицу трассировки; назвать значения переменных, которых программа не печатает. На работе так же: прежде чем поменять строку в чужом сервисе, её надо прочитать и понять, что она делает сейчас.


Компьютер не читает «по смыслу»

Человек читает программу как текст: пробегает глазами и угадывает замысел по именам. Компьютер выполняет одну строку за другой и помнит только текущие значения переменных. Почти любая ошибка чтения — подмена одного другим: «переменная называется 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

Порядок чтения

  1. Обзор. Какие пакеты подключены, какие переменные есть, где печать.
  2. Трассировка. Таблица: после каждой строки — значения всех переменных.
  3. Предсказание. Записать, что программа напечатает. Именно записать: ответ «в уме» после запуска незаметно подстраивается под увиденное.
  4. Запуск.
  5. Разбор расхождения. Не совпало — найти шаг таблицы, где ошиблись, и понять, какое правило сработало не так, как вы думали: расхождение показывает, что именно понято неверно.

Разбор: остатки на складе

Ситуация. Коллега написал программу и говорит, что она печатает свободный остаток. Прочитаем её, прежде чем верить.

Код для чтения · разбираем, набирать не нужно
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 контрольной точки.

Попробуйте сейчас: программа «остатки» в черновике.

Цель: получить вывод из блока «Что увидите», а после новой строки предсказать вывод по таблице до запуска.

  1. Черновик ~/gocourse/day05/scratch/main.go уже создан стендом. Замените всё, что в нём лежит, на эту программу — ту же, что разобрана выше:
✎ Наберите в файл scratch/main.go
   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)
   }
  1. Запустите — вывод должен совпасть с блоком «Что увидите»:
▶ Выполните
   cd ~/gocourse/day05/scratch
   go run .
  1. Теперь добавьте одну строку — free = stock - reserved сразу после stock = stock + 25. Целиком программа станет такой:
✎ Наберите в файл scratch/main.go
   package 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)
   }
  1. До запуска составьте на бумаге новую таблицу трассировки и запишите все три числа, которые программа напечатает. Потом 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 нигде не используется. Правка — одна буква в седьмой строке, и оба сообщения исчезнут. Прежде чем править, прочитайте все сообщения и найдите общую причину.


Программа, которая считает не то

Компилятор молчит, программа работает, числа неверные. Порядок:

  1. Сравнить вывод с ожидаемым построчно и найти первое неверное число.
  2. Найти строку, которая его печатает, и переменную в ней.
  3. Проследить только эту переменную вверх по программе: каждое её присваивание — строка таблицы.
  4. Найти шаг, где значение разошлось с тем, что должно быть.

Самое коварное — ошибки, о которых молчат и компилятор, и 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.go
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)
}
▶ Выполните
cd ~/gocourse/day05/scratch
go run .

Не совпало — найдите шаг таблицы, где разошлись, и отправьте ответ заново — засчитывается последний.

Готово, когда: course answer ответил Принято: day05.q1 = …, а вывод go run . в scratch совпал с отправленной строкой (пункт проверки q1).


Дальше: словарик ошибок и памятка-справочник по блоку — course next