Программа считает не то
До сих пор ошибки делились на два вида. Программа не собирается — компилятор называет файл, строку и причину. Программа получает плохой ввод — вы сами печатаете сообщение и выходите с кодом 1, как в дне 07. Сегодня третий вид, и в работе он самый частый: программа собирается, отрабатывает без единого сообщения и печатает правдоподобные, но неверные числа.
Компилятор здесь не помощник: для него всё в порядке, программа делает ровно то, что написано. Ошибка в том, что написано не то, что задумано. Такую ошибку называют дефектом, по-английски bug, а поиск и устранение дефектов — отладкой, debugging.
Править наугад — плохой путь: дефект пропадает на одном вводе и остаётся на другом. Вместо угадывания — шесть шагов, каждый со своим вопросом.
| Шаг | Вопрос | Что делаете |
|---|---|---|
| 1. Воспроизвести | На каком вводе дефект виден? | кладёте ввод в файл, записываете ожидаемое и полученное |
| 2. Сузить | Где значение впервые стало не таким? | смотрите промежуточные значения: печатью или отладчиком |
| 3. Гипотеза | Почему именно там? | называете одну причину, которую можно проверить |
| 4. Проверка | Правда ли это? | ставите опыт, исход которого назвали заранее |
| 5. Починка | Какое наименьшее изменение убирает причину? | правите одну-две строки |
| 6. Повторный прогон | Не сломалось ли остальное? | запускаете исходный ввод и все остальные примеры |
Эта страница проходит шаги на одной программе. Страницы 2–5 — инструменты для второго и четвёртого шагов, страница 6 — дефекты, которые встречаются чаще других.
Воспроизвести: ввод в файл
Ситуация. На склад пришла поставка. В накладной по строке на коробку — сколько в ней штук; пустая строка отделяет один поддон от другого. По накладной 9 коробок и 125 штук. Программа приёмки говорит другое.
Программа уже лежит в ~/gocourse/day09/priemka/main.go:
// Приёмка: сколько коробок и штук пришло по накладной.
// В накладной — по строке на коробку: число штук. Пустые строки пропускаем.
package main
import (
"bufio"
"fmt"
"os"
"strconv"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
boxes := 0
total := 0
i := 0
for scanner.Scan() {
i++
line := scanner.Text()
qty, err := strconv.Atoi(line)
if err != nil {
continue
}
boxes++
total += qty
}
fmt.Println("Коробок:", boxes)
fmt.Println("Штук:", total)
}Номера строк понадобятся на страницах 3 и 4, поэтому строки в этом файле не переставляйте. i считает строки ввода, включая пустые.
Что увидите.
stagiaire@lab:~/gocourse/day09/priemka$ cat input.txt
12
8
20
15
9
14
11
30
6
stagiaire@lab:~/gocourse/day09/priemka$ go run . < input.txt
Коробок: 8
Штук: 111Как это читать. Ожидали 9 коробок и 125 штук, получили 8 и 111. Запишите это сразу, вместе с вводом: тройка «ввод, ожидаемое, полученное» и есть описание дефекта. Так написаны условия заданий bug1–bug3.
Ввод — в файле, а не с клавиатуры: так каждый запуск получает тот же ввод и результаты можно честно сравнивать. Файл можно отдать коллеге, а после починки он же покажет, что дефекта больше нет.
Попробуйте сейчас: воспроизвести дефект priemka.
Цель: увидеть дефект своими глазами и записать тройку «ввод, ожидаемое, полученное».
1. Посмотрите ввод и запустите программу с ним:
▶ Выполнитеcd ~/gocourse/day09/priemka cat input.txt go run . < input.txt2. По
input.txtпосчитайте на бумаге, сколько должно получиться коробок и штук. Сошлось с накладной?Готово, когда: программа напечатала
Коробок: 8иШтук: 111, на бумаге записаны ожидаемые коробки и штуки, а вcourse check 09зелёный пунктt_input.
Сузить: где значение впервые не такое
На экране только итог десяти проходов. Сузить — значит найти первый проход, после которого значение переменной расходится с ожидаемым. Сначала составьте ожидание по вводу, не глядя в программу:
| Строка ввода | Текст | boxes после неё |
total после неё |
|---|---|---|---|
| 1 | 12 | 1 | 12 |
| 2 | 8 | 2 | 20 |
| 3 | 20 | 3 | 40 |
| 4 | пусто | 3 | 40 |
| 5 | 15 | 4 | 55 |
| 6 | 9 | 5 | 64 |
| 7 | 14 | 6 | 78 |
| 8 | 11 | 7 | 89 |
| 9 | 30 | 8 | 119 |
| 10 | 6 | 9 | 125 |
Настоящие значения покажут отладочная печать (страница 2) или отладчик (страницы 3–5). Когда проходов много, смотрите в середину: если там уже не так — дефект в первой половине, иначе во второй. На тысячу проходов хватает десяти взглядов. Дефект может оказаться раньше найденного места, но не позже: всё, что выполнилось после, получило уже испорченные данные.
Гипотеза и проверка
Гипотеза — одна фраза о причине, из которой следует проверяемое предсказание.
Плохая гипотеза: «что-то не так с циклом». Из неё ничего не следует, смотреть не на что.
Хорошая: «седьмую строку ввода strconv.Atoi не превращает в число, поэтому срабатывает continue на строке 22, и эта коробка пропадает». Из неё следует предсказание: на вводе из одной такой строки программа насчитает ноль коробок, а на той же строке, набранной заново, — одну.
Проверка — опыт с исходом, который вы назвали до запуска. В седьмой строке после числа стоит пробел — в выводе cat его не видно, но в файле он есть. Подадим программе строку с пробелом и без:
stagiaire@lab:~/gocourse/day09/priemka$ printf '14 \n' | go run .
Коробок: 0
Штук: 0
stagiaire@lab:~/gocourse/day09/priemka$ printf '14\n' | go run .
Коробок: 1
Штук: 14Предсказание сбылось: дело в пробеле после числа.
Правила: одна гипотеза за раз — после двух правок сразу непонятно, какая подействовала; сначала проверка, потом правка; неподтверждённую гипотезу вычеркните и вернитесь к сужению.
Минимальная починка и повторный прогон
У дефекта есть симптом — то, что видно снаружи: «не хватает 14 штук». И есть причина: пробелы по краям строки не убираются перед Atoi.
Починить симптом легко: fmt.Println("Штук:", total+14). На этой накладной ответ станет верным, на любой другой — нет: такая правка прячет дефект.
Починка причины — одна строка: убрать пробелы функцией strings.TrimSpace перед Atoi. Её вы сделаете на странице 2. Маленькую правку легко прочитать через diff, в ней негде спрятать новый дефект, и по ней видна причина. В заданиях дня лимит — одна-две строки, строки import не считаются.
Шаг 6 — прогнать исходный input.txt и все остальные примеры, включая крайние: пустой ввод, одна строка, строка с пробелами. Проверка заданий делает то же самое, и часть вводов от вас скрыта.
Попробуйте сейчас: предсказать вывод priemka.
Цель: предсказать, что напечатает
priemkaбез правок на новом вводе, и сдать ответ.1. Не запуская, предскажите, что напечатает
priemkaбез правок на таком вводе:5, пустая строка,5с пробелом в конце,5. Ответ — два числа через пробел, коробки и штуки:▶ Выполните · выделенное замените своимcourse answer day09.q1 КОРОБКИ ШТУКИВместо
КОРОБКИ— число коробок, вместоШТУКИ— число штук.2. Потом проверьте — на
priemkaбез правок. Если вы уже чинили её по странице 2, сначала верните оригинал: иначе программа посчитает верно и сверять будет не с чем:▶ Выполнитеcd ~/gocourse/day09/priemka cp ~/.course/orig/day09/priemka/main.go . printf '5\n\n5 \n5\n' | go run .Готово, когда: пункт
q1вcourse check 09зелёный.
Что может пойти не так
| Что видите | Что это значит | Что делать |
|---|---|---|
bash: inpt.txt: No such file or directory |
опечатка в имени файла; программа даже не запускалась | ls — точное имя; Tab дописывает имя сам |
| Программа ничего не печатает, приглашение не возвращается | забыли < input.txt: программа ждёт ввода с клавиатуры |
Ctrl+C — появится ^Csignal: interrupt; запустите с < |
После запуска cat input.txt ничего не печатает, ls -l показывает размер 0 |
перепутали стрелку: > input.txt сначала очищает файл, а программа ждёт клавиатуры |
Ctrl+C, затем course lab 09 — стенд положит input.txt заново |
| Вывод не такой, как на странице | main.go уже меняли |
cp ~/.course/orig/day09/priemka/main.go ~/gocourse/day09/priemka/ |
| Числа на бумаге не сошлись с накладной | ошибка в ожидании, а не в программе | пересчитайте: ожидание тоже проверяют |
Дальше: отладочная печать — course next