День 07 · Ввод и первые ошибки · страница 6 из 6

Программа с вводом целиком

Все детали на месте: прочитать строку, убрать пробелы, превратить в число, проверить ошибку, сообщить и выйти. На этой странице они складываются в одну программу вместе с условиями дня 06. И второй вопрос — как такую программу проверять: на одном удачном вводе она работает почти всегда, ломается она на неудачных.


Порядок шагов

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

  1. Прочитать строку. Ввода нет — ошибка.
  2. Убрать пробелы по краям.
  3. Превратить в число. Не число — ошибка.
  4. Проверить смысл. Число есть, но для задачи не годится: отрицательное количество, отгрузка больше остатка — ошибка.
  5. Посчитать и напечатать — только когда все проверки пройдены.

Ошибка на любом шаге — сообщение в поток ошибок и 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.txt

2. Спрашивается не текст сообщения, а его адрес: в какой из двух файлов оно попадёт — out.txt или err.txt — и сколько строк при этом напечатает на экран сама программа. Отправьте имя файла и число через пробел:

▶ Выполните · выделенное замените своим
course answer day07.q4 ИМЯ_ФАЙЛА ЧИСЛО

3. После ответа проверьте себя запуском. Теперь ту же программу нужно набрать:

✎ Наберите в файл scratch/main.go
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)
}
▶ Выполните
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.txt

2. Страховой запас у вас свой — он в условии. До того как писать код, составьте таблицу вводов: три примера из условия и ещё несколько на краях — остаток ровно равен запасу, остаток меньше запаса, заказ ровно равен тому, что можно отдать, и на единицу больше. Для каждого впишите ожидаемый вывод.

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.md

2. Допишите разборы сегодняшних сообщений, которые вы видели сами.

Готово, когда: в course check 07 пункт «Словарик ~/errors.md: минимум четырнадцать разборов» (errors) отмечен ✔.


Материал дня закончен. Задания — course lab 07, дополнительное чтение — course extra 07