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

Код возврата и поток ошибок

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


Код возврата: echo $?

Ситуация. Нужно узнать, успешно ли закончилась программа, не читая её вывод.

Программа из раздела if err != nil, с return:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ echo abc | go run .; echo $?
ошибка: нужно целое число
0

Как это читать. Любая программа, завершаясь, отдаёт системе число — код возврата. $? — код возврата последней выполненной команды, echo $? его печатает. Точка с запятой ; просто ставит две команды в одну строку: сначала первая, потом вторая.

Договорённость одна на все программы: 0 — успех, любое другое число — неудача. Здесь 0. Программа сообщила об ошибке словами, а кодом сказала «всё хорошо». Скрипт, который её запустил, поверит коду.

$? меняется после каждой команды. Второй echo $? подряд напечатает 0: это уже код первого echo, который выполнился успешно. Смотреть код нужно сразу после запуска программы.


os.Exit(1): завершиться с кодом

Код для чтения · разбираем, набирать не нужно
package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println("до выхода")
	os.Exit(1)
	fmt.Println("после выхода")
}

Что увидите.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ go run .; echo $?
до выхода
exit status 1
1
stagiaire@lab:~/gocourse/day07/scratch$ go build -o prog . && ./prog; echo $?
до выхода
1

Как это читать. os.Exit(1) завершает программу немедленно с кодом 1. Строка после выхода не напечаталась: до неё дело не дошло. go vet на такую недостижимую строку не жалуется, так что следить за этим придётся самим.

Строку exit status 1 напечатала не ваша программа, а go run: он собрал программу, запустил её, увидел ненулевой код и сообщил. Собранный файл ./prog запускается без посредника, и этой строки нет.

У go run есть особенность: при любом ненулевом коде программы он сам завершается с кодом 1. С os.Exit(3) он напишет exit status 3, а echo $? после него покажет 1. Собранный ./prog покажет 3. Поэтому точный код смотрят на собранной программе.

Чем return в main отличается от os.Exit:

return в main os.Exit(1)
строки ниже не выполняются не выполняются
код возврата 0 1
когда писать работа закончена нормально случилась ошибка

Попробуйте сейчас: код возврата своими глазами.

Цель: увидеть код os.Exit у собранной программы и у go run.

1. Наберите в scratch/main.go программу с os.Exit(1):

✎ Наберите в файл scratch/main.go
package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println("до выхода")
	os.Exit(1)
	fmt.Println("после выхода")
}

Соберите её и посмотрите код дважды подряд:

▶ Выполните
cd ~/gocourse/day07/scratch
go build -o prog .
./prog
echo $?
echo $?

2. Поменяйте 1 на 3, пересоберите и сравните echo $? после ./prog и после go run .:

▶ Выполните
go build -o prog .
./prog
echo $?
go run .
echo $?

Готово, когда: в первом шаге первый echo $? напечатал 1, второй — 0; после os.Exit(3) у ./prog код 3, а go run . написал exit status 3 и дал код 1 (пункт проверки t_status).


Два потока вывода

Ситуация. Отчёт по складу отправляют в файл > out.txt. Если в отчёт попадёт строка «нет данных за вчера», файл испорчен: программа, которая его читает, примет эту строку за данные. А если сообщение уйдёт в файл, человек его не увидит.

У программы два канала вывода:

Код для чтения · разбираем, набирать не нужно
package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println("Отчёт по складу")
	fmt.Fprintln(os.Stderr, "нет данных за вчера")
	fmt.Println("Остаток: 40")
}

Что увидите.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ go build -o report . && ./report
Отчёт по складу
нет данных за вчера
Остаток: 40
stagiaire@lab:~/gocourse/day07/scratch$ ./report > out.txt
нет данных за вчера
stagiaire@lab:~/gocourse/day07/scratch$ cat out.txt
Отчёт по складу
Остаток: 40
stagiaire@lab:~/gocourse/day07/scratch$ ./report 2> err.txt
Отчёт по складу
Остаток: 40
stagiaire@lab:~/gocourse/day07/scratch$ cat err.txt
нет данных за вчера

Как это читать. Без перенаправления оба потока идут на экран вперемешку — не отличить. > перехватывает только обычный вывод: в out.txt чистый отчёт, а сообщение осталось на экране. 2> перехватывает поток ошибок. Цифра 2 — номер потока: у обычного вывода номер 1, и > — это сокращение от 1>. Если в конвейер ушёл только обычный вывод, а поток ошибок остался на экране, строки могут показаться не в том порядке,

в каком программа их печатала: у ./report | cat строка нет данных за вчера выходит раньше «Отчёт по складу».

Fprintln — тот же Println, но с первым аргументом «куда печатать». Буква F в начале — от file: для программы поток — такой же файл, куда можно писать. os.Stderr — поток ошибок, os.Stdout — обычный вывод, os.Stdin, знакомый по scanner, — ввод.


Попробуйте сейчас: сколько строк уйдёт в файл, а сколько на экран?

Цель: предсказать, как > разложит вывод программы по двум адресам, и сдать ответ day07.q2.

1. Не запуская, прочитайте программу:

Код для чтения · разбираем, набирать не нужно
package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println("Остаток: 40")
	fmt.Fprintln(os.Stderr, "склад Юг не ответил")
	fmt.Println("Остаток: 15")
}

Её собрали в ./report и запустили так:

Образец · набирать не нужно
./report > out.txt

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

▶ Выполните · выделенное замените своим
course answer day07.q2 ЧИСЛО ЧИСЛО

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

✎ Наберите в файл scratch/main.go
package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println("Остаток: 40")
	fmt.Fprintln(os.Stderr, "склад Юг не ответил")
	fmt.Println("Остаток: 15")
}
▶ Выполните
cd ~/gocourse/day07/scratch
go build -o report .
./report > out.txt
cat out.txt

Первая команда покажет, что осталось на экране, вторая — что легло в файл; строки в обоих местах посчитайте сами.

Готово, когда: ответ сдан, и два числа из запуска совпали с вашими; в course check 07 пункт «Сколько строк уйдёт в файл и сколько останется на экране» (q2) отмечен ✔.


Вместе: сообщить и выйти

Сообщение — в поток ошибок, код — ненулевой. Это одна и та же пара строк во всех программах курса:

Код для чтения · разбираем, набирать не нужно
	qty, err := strconv.Atoi(line)
	if err != nil {
		fmt.Fprintln(os.Stderr, "ошибка:", err)
		os.Exit(1)
	}

Это образец строк из тела main, а не программа: отдельно он не соберётся, ему нужны line и пакеты strconv и os. В программе он встаёт сразу после чтения строки. С такой проверкой в fix1 на ввод abc:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/fix1$ echo abc | go run .; echo $?
ошибка: strconv.Atoi: parsing "abc": invalid syntax
exit status 1
1

Человек видит сообщение на экране, даже если результат отправлен в файл. Скрипт видит код 1 и понимает: результата нет. В файл с результатом не попало ничего лишнего.

Правило дня поменялось — вот где проходит граница. До этого места, на четвёртой странице и в задании cost, об ошибке сообщали проще: fmt.Println("ошибка: …") и return. Так было сделано нарочно: os.Stderr и os.Exit вы ещё не проходили. С этой страницы правило другое и действует до конца курса: сообщение об ошибке — в поток ошибок, выход — с ненулевым кодом, то есть fmt.Fprintln(os.Stderr, …) и os.Exit(1).

Сданный cost переделывать не нужно. Его проверка сверяет только строку со стоимостью на правильных вводах, упрощённый образец она принимает, и пункт cost от этого не покраснеет. Захотите привести cost к новому образцу — хуже не станет, но это добровольно. А fix1, accept и reserve, которые ещё впереди, пишутся уже по новому правилу: у них проверка смотрит и поток ошибок, и код возврата.

Кодов неудачи бывает несколько: программа может отвечать разными числами на разные беды, чтобы вызывающий различал их без чтения текста.

Попробуйте сейчас: чего вывод не показывает?

Цель: пройти программу по строкам и назвать значения, которых в выводе нет; сдать ответ day07.q3.

1. Не запуская, прочитайте программу. Ей подадут строку 250 с пробелом в начале:

Код для чтения · разбираем, набирать не нужно
package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	if !scanner.Scan() {
		fmt.Fprintln(os.Stderr, "ошибка: нет ввода")
		os.Exit(2)
	}
	n, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
	if err != nil {
		fmt.Fprintln(os.Stderr, "ошибка:", err)
		os.Exit(3)
	}
	boxes := n / 12
	left := n - boxes*12
	if left != 0 {
		fmt.Fprintln(os.Stderr, "ошибка: партия не делится на коробки")
		os.Exit(4)
	}
	fmt.Println("Коробок:", boxes)
}

2. Ни boxes, ни left в вывод не попадают: программа уходит в os.Exit(4) с сообщением, в котором чисел нет. Спрашиваются именно они. Отправьте два числа через пробел — значения boxes и left в тот момент, когда выполняется строка if left != 0:

▶ Выполните · выделенное замените своим
course answer day07.q3 ЧИСЛО ЧИСЛО

3. После ответа проверьте себя запуском. Наберите ту же программу, но с отладочной печатью перед проверкой — так в неё и смотрят, когда значений не видно:

✎ Наберите в файл scratch/main.go
package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	if !scanner.Scan() {
		fmt.Fprintln(os.Stderr, "ошибка: нет ввода")
		os.Exit(2)
	}
	n, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
	if err != nil {
		fmt.Fprintln(os.Stderr, "ошибка:", err)
		os.Exit(3)
	}
	boxes := n / 12
	left := n - boxes*12
	fmt.Println("отладка:", boxes, left)
	if left != 0 {
		fmt.Fprintln(os.Stderr, "ошибка: партия не делится на коробки")
		os.Exit(4)
	}
	fmt.Println("Коробок:", boxes)
}
▶ Выполните
cd ~/gocourse/day07/scratch
go build -o limit .
echo ' 250' | ./limit; echo $?

Готово, когда: ответ сдан, строка отладка: показала те же два числа, а echo $? напечатал 4 — код из той ветки, в которую программа ушла; в course check 07 пункт «Чего вывод не показывает: значения перед проверкой» (q3) отмечен ✔.


Что может пойти не так

Что видите Что это значит Что делать
в начале строки что-то вроде &{0x…} — адрес в памяти, дальше сообщение fmt.Println(os.Stderr, …): поток напечатан как обычное значение, а сообщение ушло в обычный вывод fmt.Fprintln(os.Stderr, …). go vet . ловит это: fmt.Println does not take io.Writer but has first arg os.Stderr
undefined: os.exit (but have Exit) маленькая буква os.Exit
too many return values и строки have (number), want () return 1 в main: main ничего не возвращает os.Exit(1)
exit status 1 под выводом программы это go run сообщает код, а не компилятор ругается ничего, так и должно быть при os.Exit(1)
go run пишет exit status 3, а echo $? — 1 go run сам выходит с кодом 1 точный код — на собранной программе
echo $? показывает 0, хотя программа сообщила об ошибке в программе return вместо os.Exit(1) или между запуском и echo $? была другая команда os.Exit(1); echo $? — сразу после запуска
сообщение об ошибке оказалось в out.txt печать через fmt.Println fmt.Fprintln(os.Stderr, …)
строки после os.Exit не напечатались os.Exit завершает программу сразу так и задумано: всё нужное — до os.Exit

Попробуйте сейчас: починка fix1 и задание accept.

Цель: fix1 на не число сообщает об ошибке в поток ошибок с кодом 1; accept работает по своему TASK.txt, и go vet . в нём молчит.

1. Программа fix1 собирается, но ошибку выбрасывает через _:

▶ Выполните
cd ~/gocourse/day07/fix1
echo abc | go run .; echo $?

Сейчас она печатает Заказано: 0 шт. и код 0.

2. Сделайте так, чтобы на не число было сообщение со словом «ошибка» в поток ошибок и код 1, а на число — как раньше. Не больше семи изменённых строк, строки import не считаются: минимальной починке хватает пяти, остальное — запас на второе слово в сообщении или на else. Лимит — часть пункта fix1: программу, переписанную заново, пункт не засчитает.

3. Проверьте abc, 15 и 0 — ноль тоже целое число, его программа должна принимать:

▶ Выполните
echo abc | go run .; echo $?
echo 15 | go run .; echo $?
echo 0 | go run .; echo $?

4. Потом — задание accept: приёмка поставки.

▶ Выполните
cd ~/gocourse/day07/accept
cat TASK.txt

5. Создайте main.go, потом go build -o accept . — и проверьте код возврата на каждом примере из условия. В конце — в папке accept:

▶ Выполните
go vet .

Готово, когда: fix1 на abc печатает сообщение со словом «ошибка», exit status 1 и код 1, на 15 и 0 — строку Заказано: с этим числом и код 0; go vet . в accept ничего не печатает (пункты проверки fix1, accept, accept_vet).


Дальше: программа с вводом целиком — course next