День 09 · Отладчик и поиск ошибок · страница 1 из 6

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

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

2. По 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