День 12 · Несколько результатов и ошибка как результат · страница 4 из 7

Кто вызвал, тот и решает

Функция из прошлой страницы умеет сказать «не смогла». Но она не знает, что с этим делать: остановить программу, пропустить строку, спросить человека — зависит от того, где её вызвали. Поэтому функция возвращает ошибку, а решение принимает тот, кто её вызвал. Эта страница — про вторую половину разговора.


Проверка сразу под вызовом

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

Код для чтения · разбираем, набирать не нужно
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.txt

2. Напишите main.go: функция parseQty(s string) (int, error) и main, который её вызывает. Три проверки внутри функции идут по порядку — пустая строка, не число, число меньше единицы.

3. Проверьте на примерах и на пустом вводе, а код возврата посмотрите сразу после запуска:

▶ Выполните
go run . < example1.txt
go run . < example2.txt; echo $?
go run . < /dev/null

4. Перед сдачей — привычные две команды:

▶ Выполните
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