Кто вызвал, тот и решает
Функция из прошлой страницы умеет сказать «не смогла». Но она не знает, что с этим делать: остановить программу, пропустить строку, спросить человека — зависит от того, где её вызвали. Поэтому функция возвращает ошибку, а решение принимает тот, кто её вызвал. Эта страница — про вторую половину разговора.
Проверка сразу под вызовом
Ситуация. Приёмка читает строки до конца ввода. Первая же плохая строка — повод остановиться: считать «принято столько-то», когда часть накладной не разобралась, нельзя.
package main
import (
"bufio"
"errors"
"fmt"
"os"
"strconv"
"strings"
)
func parseQty(s string) (int, error) {
text := strings.TrimSpace(s)
if text == "" {
return 0, errors.New("пустое количество")
}
qty, err := strconv.Atoi(text)
if err != nil {
return 0, fmt.Errorf("не число: %v", err)
}
if qty <= 0 {
return 0, errors.New("количество должно быть больше нуля")
}
return qty, nil
}
func main() {
scanner := bufio.NewScanner(os.Stdin)
line := 0
count := 0
total := 0
for scanner.Scan() {
line++
qty, err := parseQty(scanner.Text())
if err != nil {
fmt.Fprintf(os.Stderr, "Ошибка в строке %d: %v\n", line, err)
os.Exit(1)
}
count++
total += qty
}
fmt.Printf("Принято: позиций %d, всего %d шт.\n", count, total)
}Что увидите. В example1.txt две строки — 12 и 5; в example2.txt вторая строка семь.
stagiaire@lab:~/gocourse/day12/intake$ go run . < example1.txt
Принято: позиций 2, всего 17 шт.
stagiaire@lab:~/gocourse/day12/intake$ go run . < example2.txt
Ошибка в строке 2: не число: strconv.Atoi: parsing "семь": invalid syntax
exit status 1
stagiaire@lab:~/gocourse/day12/intake$ go run . < /dev/null
Принято: позиций 0, всего 0 шт.Как это читать. Вся обработка ошибки — четыре строки, и все они уже знакомы по дню 07:
| Строка | Что делает |
|---|---|
qty, err := parseQty(scanner.Text()) |
забирает оба результата |
if err != nil { |
спрашивает, была ли ошибка |
fmt.Fprintf(os.Stderr, …) |
сообщение человеку — в поток ошибок |
os.Exit(1) |
код возврата для того, кто запустил |
Новое здесь одно: parseQty — своя функция, а не чужая. Для вызывающего разницы нет, и это главная мысль дня: ваши функции выглядят для остальной программы так же, как strconv.Atoi.
Строку if err != nil пишут сразу под вызовом, до того как результат пошёл в дело. Между вызовом и проверкой не должно быть ничего: любая строка, вставленная туда, работает с непроверенным значением.
%v в Fprintf — тот же глагол, что в дне 03: «напечатай значение как есть». Для ошибки это её текст.
fmt.Errorf: добавить, где случилось
errors.New делает ошибку с постоянным текстом. Чаще нужно другое: взять ошибку, которую вернул кто-то ниже, и добавить к ней своё — где именно это случилось.
qty, err := strconv.Atoi(text)
if err != nil {
return 0, fmt.Errorf("не число: %v", err)
}Это строки из parseQty выше. fmt.Errorf устроена как Printf: первый аргумент — шаблон, дальше значения. Только она не печатает, а возвращает ошибку с получившимся текстом. Ошибка Atoi встаёт на место %v, и в итоге получается:
не число: strconv.Atoi: parsing "семь": invalid syntaxА main добавляет своё — номер строки:
Ошибка в строке 2: не число: strconv.Atoi: parsing "семь": invalid syntaxПолучается цепочка: каждый уровень дописывает то, что знает только он. Atoi знает строку, parseQty знает, что это было количество, main знает номер строки во вводе. Ни один из них не знает всего — и не должен.
Правило простое: добавляйте то, чего нет в исходной ошибке. Писать fmt.Errorf("ошибка: %v", err) бессмысленно — слово «ошибка» ничего не уточняет.
В описании fmt.Errorf упоминается глагол %w — он тоже вставляет ошибку в текст, но вдобавок сохраняет её внутри, чтобы потом можно было спросить «а не такая-то ли это была ошибка». Спрашивать пока нечем: инструменты для этого — в блоке 5. До тех пор пишем %v.
Чего не должна делать функция
Не печатать вместо возврата. Соблазн понятный: ошибка обнаружена внутри, там же и сообщить. Получается два сообщения на одну беду — и одно из них не там, где нужно:
package main
import (
"bufio"
"fmt"
"os"
"strconv"
)
func parseQty(s string) (int, error) {
qty, err := strconv.Atoi(s)
if err != nil {
fmt.Println("ошибка: не число", s)
return 0, err
}
return qty, nil
}
func main() {
scanner := bufio.NewScanner(os.Stdin)
line := 0
for scanner.Scan() {
line++
qty, err := parseQty(scanner.Text())
if err != nil {
fmt.Fprintf(os.Stderr, "Ошибка в строке %d: %v\n", line, err)
os.Exit(1)
}
fmt.Println("приняли:", qty)
}
}stagiaire@lab:~/gocourse/day12/scratch$ printf '12\nx\n' | go run .
приняли: 12
ошибка: не число x
Ошибка в строке 2: strconv.Atoi: parsing "x": invalid syntax
exit status 1Строка ошибка: не число x ушла в обычный вывод — тот самый, который в дне 07 мы старались не пачкать: туда идёт результат работы, а не жалобы. Строка Ошибка в строке 2: … — как надо, она в потоке ошибок. Про одну беду напечатано дважды, и один раз — не там. Функция должна возвращать, печатает — вызывающий.
Не завершать программу. Ещё более вредный вариант — os.Exit(1) внутри функции:
package main
import (
"bufio"
"fmt"
"os"
"strconv"
)
func parseQty(s string) int {
qty, err := strconv.Atoi(s)
if err != nil {
fmt.Fprintln(os.Stderr, "ошибка:", err)
os.Exit(1)
}
return qty
}
func main() {
scanner := bufio.NewScanner(os.Stdin)
fmt.Println("начали")
for scanner.Scan() {
qty := parseQty(scanner.Text())
fmt.Println("приняли:", qty)
}
fmt.Println("готово")
}stagiaire@lab:~/gocourse/day12/scratch$ printf '12\nx\n' | go run .
начали
приняли: 12
ошибка: strconv.Atoi: parsing "x": invalid syntax
exit status 1Строка готово не напечаталась, и второго приняли: тоже нет: os.Exit завершил программу прямо из функции, до возврата в main дело не дошло. Пока программа маленькая, это кажется удобным. Но такую функцию нельзя вызвать там, где плохая строка — не конец света: например, пропустить её и читать дальше. Решение за вызывающего уже принято, и поменять его можно только переписав функцию.
Правило курса: os.Exit живёт в main. Функции возвращают ошибки.
Что может пойти не так
go vet проверяет шаблоны Errorf так же, как шаблоны Printf:
package main
import (
"fmt"
"strconv"
)
func parseQty(s string) (int, error) {
qty, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("количество %d: %v", qty)
}
return qty, nil
}
func main() {
fmt.Println(parseQty("x"))
}В шаблоне два глагола, %d и %v, а значение одно — qty. Компилятор такое пропускает, go vet — нет:
stagiaire@lab:~/gocourse/day12/scratch$ go vet .
main.go:11:50: fmt.Errorf format %v reads arg #2, but call has 1 arg
stagiaire@lab:~/gocourse/day12/scratch$ go run .
0 количество 0: %!v(MISSING)| Что видите | Что это значит | Что делать |
|---|---|---|
fmt.Errorf format %v reads arg #2, but call has 1 arg |
в шаблоне глаголов больше, чем значений | добавить значение или убрать глагол |
в тексте ошибки видно %!v(MISSING) |
то же самое, но программа уже запущена | go vet . находит это до запуска |
| два сообщения об одной беде | функция печатает и возвращает ошибку | печатает только вызывающий |
| строки после вызова функции не выполняются | внутри функции os.Exit |
функция возвращает ошибку, os.Exit — в main |
сообщение об ошибке попало в out.txt вместе с результатом |
печать через fmt.Println, а не Fprintln(os.Stderr, …) |
поток ошибок, как в дне 07 |
declared and not used: err |
ошибку забрали и не проверили | if err != nil сразу под вызовом |
| программа печатает итог, хотя строка была плохая | после сообщения нет os.Exit(1) или return |
выйти, а не считать дальше |
Ошибка в строке 0 |
счётчик строк увеличивается не первой строкой тела цикла | line++ — сразу после for scanner.Scan() { |
Попробуйте сейчас: задание intake.
Цель:
intake/main.goпринимает количества, а на плохой строке сообщает в поток ошибок и выходит с кодом 1.1. Прочитайте условие — тексты ошибок там дословные:
▶ Выполнитеcd ~/gocourse/day12/intake cat TASK.txt2. Напишите
main.go: функцияparseQty(s string) (int, error)иmain, который её вызывает. Три проверки внутри функции идут по порядку — пустая строка, не число, число меньше единицы.3. Проверьте на примерах и на пустом вводе, а код возврата посмотрите сразу после запуска:
▶ Выполнитеgo run . < example1.txt go run . < example2.txt; echo $? go run . < /dev/null4. Перед сдачей — привычные две команды:
▶ Выполнитеgofmt -w . go vet .5. Задание готово — закоммитьте:
▶ Выполнитеcd ~/gocourse/day12 git add . git commit -m "intake: приёмка с проверкой количества"Готово, когда: на
example1.txtнапечатаноПринято: позиций 2, всего 17 шт., наexample2.txt— строкаОшибка в строке 2: …и код1, на пустом вводе —Принято: позиций 0, всего 0 шт.;go vet .молчит. Пунктыintake,intake_vetиcode_fmtвcourse check 12зелёные.
Дальше: := против = — как появляется вторая переменная с тем же именем — course next