Код возврата и поток ошибок
Программа с прошлой страницы на плохой ввод печатает «ошибка» и завершается. Человеку этого достаточно: он прочитал и понял. Но программы часто запускает не человек, а другая программа: скрипт ночной выгрузки, система сборки, проверка курса. Читать русский текст и искать в нём слово «ошибка» она не станет. Для неё у каждой программы есть два отдельных сигнала — код возврата и поток ошибок.
Код возврата: 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.gopackage 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. Если в отчёт попадёт строка «нет данных за вчера», файл испорчен: программа, которая его читает, примет эту строку за данные. А если сообщение уйдёт в файл, человек его не увидит.
У программы два канала вывода:
- обычный вывод (stdout) — результат работы. Туда печатает
fmt.Println; - поток ошибок (stderr) — сообщения о проблемах. Туда печатает
fmt.Fprintln(os.Stderr, …).
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.txt2. Спрашивается не текст строк, а их число: сколько строк окажется в
out.txtи сколько строк программа напечатает на экран. Отправьте два числа через пробел — сначала строки в файле, потом строки на экране:▶ Выполните · выделенное замените своимcourse answer day07.q2 ЧИСЛО ЧИСЛО3. После ответа проверьте себя запуском. Теперь ту же программу нужно набрать:
✎ Наберите в файлscratch/main.gopackage 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.gopackage 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.txt5. Создайте
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