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

Ошибка — это значение

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


Что лежит в err

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

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

import (
	"fmt"
	"strconv"
)

func main() {
	qty, err := strconv.Atoi("40")
	fmt.Println(qty, err, err == nil)
	qty, err = strconv.Atoi("сорок")
	fmt.Println(qty, err, err == nil)
	fmt.Println("ошибка:", err)
}

Что увидите.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ go run .
40 <nil> true
0 strconv.Atoi: parsing "сорок": invalid syntax false
ошибка: strconv.Atoi: parsing "сорок": invalid syntax

Как это читать. У err тип error. Значение у переменной такого типа одно из двух:

Во второй строке кода =, а не :=: обе переменные уже объявлены, в них кладутся новые значения. Ошибку можно напечатать, как любую переменную, — Println сам достанет из неё текст. Пользователю такой текст полезен: в нём видна сама строка, которую не удалось разобрать, вместе с пробелами.


if err != nil

Ситуация. Если количество не число, считать сумму бессмысленно. Нужно сообщить и остановиться.

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

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

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()
	qty, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
	if err != nil {
		fmt.Println("ошибка: нужно целое число")
		return
	}
	fmt.Println("Сумма, коп.:", qty*250)
}

Что увидите.

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

Как это читать. Трассировка для ввода abc:

Строка Что происходит
12 создан scanner
13 прочитана строка abc
14 TrimSpace вернул abc, Atoi вернул 0 и ошибку: qty = 0, err — описание
15 err != nil — true, заходим внутрь
16 печать сообщения
17 return — main закончилась, программа завершена

Строка 19 не выполнилась. Для ввода 12 на строке 15 условие ложно, блок пропускается, выполняется строка 19. Пробелы по краям больше не мешают: их убрал TrimSpace.

!= читается «не равно»: «если ошибка не пустая». Это самая частая строка в программах на Go. Привыкайте писать её сразу под вызовом, который вернул err, — до того, как значение пошло в дело.

Без return проверка бесполезна. Уберите строку 17 — программа сообщит об ошибке и пойдёт дальше:

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

Человек увидит «ошибку» и тут же сумму, а другая программа, которая читает вывод, получит строку с нулём и сочтёт её настоящей.


Попробуйте сейчас: if err != nil на пяти вводах.

Цель: предсказать вывод программы с проверкой err на разных вводах и увидеть, что без return проверка бесполезна.

1. Наберите в scratch/main.go программу с if err != nil:

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

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

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()
	qty, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
	if err != nil {
		fmt.Println("ошибка: нужно целое число")
		return
	}
	fmt.Println("Сумма, коп.:", qty*250)
}

2. Запустите с вводами abc, 12, 12 , 1.5 и без ввода — go run . < /dev/null. Для каждого сначала предскажите вывод:

▶ Выполните
cd ~/gocourse/day07/scratch
echo abc | go run .
echo 12 | go run .
echo '  12  ' | go run .
echo 1.5 | go run .
go run . < /dev/null

3. Потом уберите return и повторите с abc:

▶ Выполните
echo abc | go run .

Готово, когда: все пять выводов совпали с предсказаниями, а без return на abc напечатались две строки — ошибка: нужно целое число и Сумма, коп.: 0.


Почему нельзя просто не смотреть: _

Компилятор не даёт оставить err без внимания: declared and not used: err. Но есть способ его «успокоить» — выбросить второй результат знаком подчёркивания _. Он означает «здесь было значение, оно мне не нужно»:

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

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

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()
	qty, _ := strconv.Atoi(scanner.Text())
	fmt.Println("Сумма, коп.:", qty*250)
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ echo abc | go run .
Сумма, коп.: 0

Программа собирается, ничего не сообщает и выдаёт сумму ноль. На складе это заказ на ноль рублей, который ушёл дальше как настоящий. Такую ошибку потом ищут днями, потому что в месте, где она случилась, не осталось никакого следа.

Правило курса: _ на месте err не пишем. Ошибка, которую «точно не может быть», всё равно проверяется — это одна строка if и одна строка сообщения.


Два числа — две проверки

Ситуация. С ввода приходят цена и количество. Каждое превращение может не получиться, и у каждого своя проверка.

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

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

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()
	price, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
	if err != nil {
		fmt.Println("ошибка: цена —", err)
		return
	}
	scanner.Scan()
	qty, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
	if err != nil {
		fmt.Println("ошибка: количество —", err)
		return
	}
	fmt.Println("Стоимость, коп.:", price*qty)
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ printf '250\n4\n' | go run .
Стоимость, коп.: 1000
stagiaire@lab:~/gocourse/day07/scratch$ printf '250\nчетыре\n' | go run .
ошибка: количество — strconv.Atoi: parsing "четыре": invalid syntax

Во втором := переменная err уже есть. Это разрешено: := требует, чтобы хотя бы одна переменная слева была новой. qty — новая, её объявят, а в err просто положат новое значение. Если же обе переменные слева уже существуют, получится знакомая ошибка:

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

import (
	"fmt"
	"strconv"
)

func main() {
	price, err := strconv.Atoi("250")
	qty, err := strconv.Atoi("4")
	fmt.Println(price*qty, err)
	price, err := strconv.Atoi("300")
	fmt.Println(price)
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ go run .
# day07/scratch
./main.go:12:13: no new variables on left side of :=

В строке 12 и price, и err уже объявлены — нужен =.

В сообщении об ошибке полезно назвать, что не разобралось: цена или количество. Текст из err говорит, какая строка была плохой, но не говорит, где она стояла во вводе.


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

Что видите Что это значит Что делать
invalid operation: err != 0 (mismatched types error and untyped int) err сравнили с нулём, а это не число err != nil
напечатана ошибка, а ниже ещё строка с результатом после сообщения нет return return последней строкой внутри if
программа молча печатает 0 на неправильный ввод ошибка выброшена через _ или не проверена qty, err := и if err != nil
declared and not used: err ошибку получили и не проверили if err != nil { … } под вызовом
undefined: err в проверке err, а слева от := вместо него _ qty, err :=
no new variables on left side of := обе переменные слева уже объявлены = вместо :=
strconv.Atoi: parsing "": invalid syntax строка пустая: пустая строка во вводе или ввода не было сообщение об ошибке — это правильная реакция

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

Цель: первая программа с проверками — cost/main.go читает цену и количество и печатает стоимость в рублях и копейках.

1. Прочитайте условие:

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

2. Создайте в этой папке main.go: две строки ввода — цена в копейках и количество, одна строка вывода — стоимость в рублях и копейках. Как разложить копейки на рубли и копейки и как напечатать 05, вы видели в дне 03: /, % и %02d в Printf.

3. Запустите на двух вводах:

▶ Выполните
printf '1250\n3\n' | go run .
printf '  5 \n 1\n' | go run .

Второй запуск должен дать:

Вывод · это печатает программа, набирать не нужно
Стоимость: 0 руб. 05 коп.

Проверка подаст программе и другие вводы — те, которых в условии нет.

Готово, когда: второй запуск напечатал ровно эту строку, и в course check 07 пункт «cost печатает стоимость позиции по цене и количеству с ввода» (cost) отмечен ✔.


Дальше: код возврата и поток ошибок — course next