Программа с вводом целиком
Все детали на месте: прочитать строку, убрать пробелы, превратить в число, проверить ошибку, сообщить и выйти. На этой странице они складываются в одну программу вместе с условиями дня 06. И второй вопрос — как такую программу проверять: на одном удачном вводе она работает почти всегда, ломается она на неудачных.
Порядок шагов
Программа, которая читает данные, почти всегда устроена одинаково. Каждый шаг может не получиться, и после каждого, который может не получиться, стоит проверка:
- Прочитать строку. Ввода нет — ошибка.
- Убрать пробелы по краям.
- Превратить в число. Не число — ошибка.
- Проверить смысл. Число есть, но для задачи не годится: отрицательное количество, отгрузка больше остатка — ошибка.
- Посчитать и напечатать — только когда все проверки пройдены.
Ошибка на любом шаге — сообщение в поток ошибок и os.Exit(1). Это правило с пятой страницы, и оно заменило упрощённый образец четвёртой (Println и return), по которому написан cost: переделывать cost не нужно, а всё, что пишется дальше, — по этому правилу. До пятого шага программа ничего не печатает в обычный вывод, поэтому при ошибке в out.txt не остаётся половины результата.
Пример: отгрузка со склада
Ситуация. На ввод приходят остаток товара и количество к отгрузке. Отгрузить больше, чем есть, нельзя.
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
if !scanner.Scan() {
fmt.Fprintln(os.Stderr, "ошибка: нет остатка")
os.Exit(1)
}
stock, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
if err != nil {
fmt.Fprintln(os.Stderr, "ошибка: остаток —", err)
os.Exit(1)
}
if !scanner.Scan() {
fmt.Fprintln(os.Stderr, "ошибка: нет количества к отгрузке")
os.Exit(1)
}
shipped, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
if err != nil {
fmt.Fprintln(os.Stderr, "ошибка: отгрузка —", err)
os.Exit(1)
}
if shipped > stock {
fmt.Fprintln(os.Stderr, "ошибка: отгрузка", shipped, "шт. больше остатка", stock, "шт.")
os.Exit(1)
}
fmt.Println("Отгружено:", shipped, "шт.")
fmt.Println("Остаток:", stock-shipped, "шт.")
}Что увидите.
stagiaire@lab:~/gocourse/day07/scratch$ printf '40\n15\n' | go run .
Отгружено: 15 шт.
Остаток: 25 шт.
stagiaire@lab:~/gocourse/day07/scratch$ printf '40\n55\n' | go run .; echo $?
ошибка: отгрузка 55 шт. больше остатка 40 шт.
exit status 1
1
stagiaire@lab:~/gocourse/day07/scratch$ printf '40\n' | go run .; echo $?
ошибка: нет количества к отгрузке
exit status 1
1
stagiaire@lab:~/gocourse/day07/scratch$ printf ' 40 \nпятнадцать\n' | go run .; echo $?
ошибка: отгрузка — strconv.Atoi: parsing "пятнадцать": invalid syntax
exit status 1
1Как это читать. Пустые строки в коде делят программу на куски по шагам: одно число — чтение и проверка, второе число — то же самое, потом проверка смысла, потом результат. Каждый кусок читается сверху вниз и заканчивается либо выходом, либо переходом к следующему.
Сообщения называют, что не так и с каким значением: не просто «ошибка», а «отгрузка 55 шт. больше остатка 40 шт.». Человек, который увидит это ночью в журнале, поймёт, что исправлять, не открывая код.
Условие shipped > stock — граница. Отгрузить ровно столько, сколько есть, можно, и остаток станет 0. Как в дне 06 с > и >=: здесь знак выбран так, что «ровно столько же» — не ошибка.
Проверять несколькими вводами
Удачный ввод почти ничего не доказывает. Ошибки прячутся на краях: ровно на границе, на единицу за ней, в пустом вводе, в пробелах. Удобно заранее записать вводы в файлы и прогнать программу по каждому.
stagiaire@lab:~/gocourse/day07/scratch$ printf '40\n15\n' > input1.txt
stagiaire@lab:~/gocourse/day07/scratch$ printf '40\n40\n' > input2.txt
stagiaire@lab:~/gocourse/day07/scratch$ printf '40\n41\n' > input3.txt
stagiaire@lab:~/gocourse/day07/scratch$ go build -o ship .| Файл | Что проверяет | Ожидаем | Код |
|---|---|---|---|
input1.txt |
обычный случай | отгружено 15, остаток 25 | 0 |
input2.txt |
ровно на границе | отгружено 40, остаток 0 | 0 |
input3.txt |
на единицу за границей | сообщение об ошибке | 1 |
stagiaire@lab:~/gocourse/day07/scratch$ ./ship < input1.txt; echo $?
Отгружено: 15 шт.
Остаток: 25 шт.
0
stagiaire@lab:~/gocourse/day07/scratch$ ./ship < input2.txt; echo $?
Отгружено: 40 шт.
Остаток: 0 шт.
0
stagiaire@lab:~/gocourse/day07/scratch$ ./ship < input3.txt; echo $?
ошибка: отгрузка 41 шт. больше остатка 40 шт.
1
stagiaire@lab:~/gocourse/day07/scratch$ ./ship < input3.txt > out.txt; echo $?
ошибка: отгрузка 41 шт. больше остатка 40 шт.
1
stagiaire@lab:~/gocourse/day07/scratch$ cat out.txt
stagiaire@lab:~/gocourse/day07/scratch$Последний запуск показывает, зачем поток ошибок: вывод отправлен в файл, а сообщение всё равно на экране, и out.txt пустой — ни половины отчёта, ни текста ошибки в нём нет.
Таблица «ввод — ожидаем — код» пишется до запуска. Если сначала запустить, а потом решать, правильно ли, любой вывод кажется правдоподобным. Проверка курса делает то же самое: у неё есть такие таблицы для каждого задания, и часть строк в них вы не видите.
Какие вводы добавлять к обычному:
- ровно на границе условия и на единицу по обе стороны;
- ноль и отрицательное число;
- пустая строка и отсутствие ввода;
- пробелы и табуляции вокруг числа;
- не число: слово, дробь, число с пробелом внутри.
Попробуйте сейчас: куда уйдёт сообщение об ошибке?
Цель: предсказать, где окажется сообщение у программы, написанной не по правилу дня, и сдать ответ
day07.q4.1. Не запуская, прочитайте программу. Об ошибке она сообщает
fmt.Printlnи заканчиваетreturn— ни того, ни другого правило пятой страницы не разрешает:Код для чтения · разбираем, набирать не нужноpackage main import ( "bufio" "fmt" "os" "strconv" ) func main() { scanner := bufio.NewScanner(os.Stdin) scanner.Scan() qty, err := strconv.Atoi(scanner.Text()) if err != nil { fmt.Println("ошибка ввода") return } fmt.Println("заказ:", qty) }Её собрали в
./orderи запустили так —echo 3 5даёт одну строку3 5с пробелом внутри, а результат отправлен в файл и в файл же отправленerr.txt:Образец · набирать не нужноecho 3 5 | ./order > out.txt 2> err.txt2. Спрашивается не текст сообщения, а его адрес: в какой из двух файлов оно попадёт —
out.txtилиerr.txt— и сколько строк при этом напечатает на экран сама программа. Отправьте имя файла и число через пробел:▶ Выполните · выделенное замените своимcourse answer day07.q4 ИМЯ_ФАЙЛА ЧИСЛО3. После ответа проверьте себя запуском. Теперь ту же программу нужно набрать:
✎ Наберите в файлscratch/main.gopackage main import ( "bufio" "fmt" "os" "strconv" ) func main() { scanner := bufio.NewScanner(os.Stdin) scanner.Scan() qty, err := strconv.Atoi(scanner.Text()) if err != nil { fmt.Println("ошибка ввода") return } fmt.Println("заказ:", qty) }▶ Выполнитеcd ~/gocourse/day07/scratch go build -o order . echo 3 5 | ./order > out.txt 2> err.txt echo $? cat out.txt cat err.txtГотово, когда: ответ сдан; сообщение нашлось в том файле, который вы назвали, второй файл оказался пустым, а
echo $?напечатал0— то есть вызывающий скрипт принял бы файл с текстом ошибки за отчёт; вcourse check 07пункт «Куда попадёт сообщение при Println и return» (q4) отмечен ✔.
Что может пойти не так
| Что видите | Что это значит | Что делать |
|---|---|---|
на неудачный ввод в out.txt оказалась первая строка результата |
печать до того, как пройдены все проверки | сначала все проверки, потом печать |
| на ввод, равный границе, программа пишет ошибку | в условии >= вместо > или наоборот |
проверить границу отдельным файлом ввода |
| проверка курса пишет «случай 5 из 8» со вводом, которого нет в условии | это скрытый случай: программа ошиблась на крае | взять ввод из сообщения, прогнать руками, сравнить с условием |
| на скрытом случае с пробелами вокруг числа — ошибка | нет strings.TrimSpace |
TrimSpace перед Atoi |
| в выводе лишнее «Введите число:» | приглашение попало в обычный вывод | в заданиях приглашений нет — только результат |
no new variables on left side of := на втором числе |
обе переменные слева уже объявлены | новое имя для числа или = |
Попробуйте сейчас: задание reserve.
Цель:
reserve/main.goсчитает резерв под заказ по своемуTASK.txtи не ошибается на краях.1. Прочитайте условие:
▶ Выполнитеcd ~/gocourse/day07/reserve cat TASK.txt2. Страховой запас у вас свой — он в условии. До того как писать код, составьте таблицу вводов: три примера из условия и ещё несколько на краях — остаток ровно равен запасу, остаток меньше запаса, заказ ровно равен тому, что можно отдать, и на единицу больше. Для каждого впишите ожидаемый вывод.
3. Потом создайте
main.goи прогоните таблицу через файлы илиprintf.Порядок проверок в цепочке
if … else if— как в дне 06: первая истинная ветка выполняется, остальные не смотрятся.Готово, когда: на каждом вводе из таблицы программа напечатала то, что вы вписали заранее, и в
course check 07пункт «reserve считает резерв с учётом страхового запаса» (reserve) отмечен ✔.
Словарик
Сегодня ошибок было много, и не все из них — от компилятора. Рядом с undefined и declared and not used появились сообщения программ: strconv.Atoi: parsing …, exit status 1, signal: interrupt, bash: …: No such file or directory, и сообщения go vet. Их тоже стоит записать: через месяц вы будете узнавать их по первым словам.
К концу дня в ~/errors.md должно быть минимум четырнадцать разборов — вместе с записями прошлых дней. Строка — текст ошибки, тире, что это значило:
assignment mismatch: 1 variable but strconv.Atoi returns 2 values — Atoi возвращает число и ошибку, нужно qty, err :=
strconv.Atoi: parsing "12 ": invalid syntax — пробел в конце строки, забыл strings.TrimSpace
fmt.Println does not take io.Writer but has first arg os.Stderr — написал Println вместо FprintlnЭто образцы формата: записывайте сообщения, которые видели сами, и что они значили у вас.
Кандидаты дня — в таблицах «Что может пойти не так» на каждой странице.
Попробуйте сейчас: словарик дня.
Цель: в
~/errors.mdне меньше четырнадцати разборов вместе с прошлыми днями.1. Откройте словарик:
▶ Выполнитеcode ~/errors.md2. Допишите разборы сегодняшних сообщений, которые вы видели сами.
Готово, когда: в
course check 07пункт «Словарик ~/errors.md: минимум четырнадцать разборов» (errors) отмечен ✔.
Материал дня закончен. Задания — course lab 07, дополнительное чтение — course extra 07