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

Ошибка как результат своей функции

В дне 07 вы приняли ошибку от чужой функции: strconv.Atoi возвращает число и error, а вы проверяли if err != nil. Тогда это выглядело особенностью Atoi. На самом деле это обычная функция с двумя результатами — ровно такая, какую вы научились писать на первой странице. Значит, свои функции тоже могут сообщать о неудаче — и должны, если могут не справиться.


Функция, которая может не справиться

Ситуация. На приёмке товар считают вручную и записывают количество строкой. В строке бывает что угодно: пусто, семь, 0, -3. Нужна функция, которая либо отдаст нормальное количество, либо честно скажет, что не смогла.

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

import (
	"errors"
	"fmt"
	"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, err
	}
	if qty <= 0 {
		return 0, errors.New("количество должно быть больше нуля")
	}
	return qty, nil
}

func main() {
	fmt.Println(parseQty(" 12 "))
	fmt.Println(parseQty(""))
	fmt.Println(parseQty("12шт"))
	fmt.Println(parseQty("0"))
}

Что увидите.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day12/scratch$ go run .
12 <nil>
0 пустое количество
0 strconv.Atoi: parsing "12шт": invalid syntax
0 количество должно быть больше нуля

Как это читать. В заголовке два результата: (int, error). Тип error — тот же самый, что у err из дня 07; ничего нового объявлять не нужно, он встроен в язык.

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


Соглашение, которое соблюдают все

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

  1. Ошибка — последний результат. (int, error), а не (error, int).
  2. Если ошибка не nil, остальные результаты бессмысленны — обычно нулевые: 0, "", false. Брать их нельзя.
  3. Если ошибка nil, результат настоящий.

Второй пункт объясняет, почему в parseQty везде return 0, …. Можно было бы вернуть то, что успело разобраться, — но тогда вызывающий, забывший проверить ошибку, получил бы правдоподобный мусор. Ноль в такой ситуации хотя бы заметнее.

Соблюдать соглашение нужно даже там, где компилятор не мешает нарушить. Вот функция, которая возвращает ошибку первой, — она прекрасно собирается:

Код для чтения · разбираем, набирать не нужно
func parseQty(s string) (error, int) {
	if s == "" {
		return errors.New("пустое количество"), 0
	}
	return nil, len(s)
}

Компилятор доволен, а любой, кто будет вызывать её по привычке как qty, err := parseQty(s), получит либо ошибку компиляции, либо — что хуже — работающую программу с перепутанными смыслами. Пишите ошибку последней.

Если же перепутать порядок в return, оставив правильный заголовок, компилятор всё-таки вмешается:

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

import (
	"errors"
	"fmt"
)

func parseQty(s string) (int, error) {
	if s == "" {
		return errors.New("пустое количество"), 0
	}
	return len(s), nil
}

func main() {
	fmt.Println(parseQty(""))
}
Вывод · это печатает программа, набирать не нужно
# day12/scratch
./main.go:10:10: cannot use errors.New("пустое количество") (value of interface type error) as int value in return statement
./main.go:10:59: cannot use 0 (constant of type int) as error value in return statement: int does not implement error (missing method Error)

Два сообщения на одну строку: первое значение не подходит под int, второе — под error. Слова interface type и missing method Error разберём в блоке 5; пока читайте их как «ошибка — не число, число — не ошибка».


Попробуйте сейчас: что вернёт своя функция.

Цель: предсказать оба результата функции с ошибкой и сдать ответ.

1. Прочитайте программу. Что напечатает fmt.Println(qty, err != nil)?

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

import (
	"errors"
	"fmt"
	"strconv"
)

func parseQty(s string) (int, error) {
	qty, err := strconv.Atoi(s)
	if err != nil {
		return 0, errors.New("не число")
	}
	return qty, nil
}

func main() {
	qty, err := parseQty("12шт")
	fmt.Println(qty, err != nil)
}

2. Сдайте ответ — число и слово через пробел, ровно как напечатает программа:

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

3. Проверьте запуском — наберите ту же программу в scratch/main.go:

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

import (
	"errors"
	"fmt"
	"strconv"
)

func parseQty(s string) (int, error) {
	qty, err := strconv.Atoi(s)
	if err != nil {
		return 0, errors.New("не число")
	}
	return qty, nil
}

func main() {
	qty, err := parseQty("12шт")
	fmt.Println(qty, err != nil)
}
▶ Выполните
cd ~/gocourse/day12/scratch
go run .

Готово, когда: пункт q3 в course check 12 зелёный.


errors.New и что у ошибки внутри

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day12/scratch$ go doc errors.New
package errors // import "errors"

func New(text string) error
    New returns an error that formats as the given text. Each call to New
    returns a distinct error value even if the text is identical.

«Каждый вызов возвращает отдельное значение, даже если текст тот же» — из этого следует важное: сравнивать ошибки между собой знаком == бесполезно, две ошибки с одинаковым текстом не равны. Как проверять, «та ли это ошибка», разберём в блоке 5. Пока ошибку проверяют одним способом — err != nil.

Печатается ошибка как обычное значение: Println и %v в Printf берут из неё текст.

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

import (
	"errors"
	"fmt"
)

func check(qty int) error {
	if qty <= 0 {
		return errors.New("количество должно быть больше нуля")
	}
	return nil
}

func main() {
	err := check(5)
	fmt.Println(err, err == nil)
	err = check(0)
	fmt.Println(err, err == nil)
	fmt.Println("ошибка:", err)
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day12/scratch$ go run .
<nil> true
количество должно быть больше нуля false
ошибка: количество должно быть больше нуля

Здесь у функции один результат — только ошибка. Так пишут проверки, у которых нет полезного значения: «годится или нет». Слово error в заголовке стоит там же, где раньше стоял int.

Текст ошибки пишут с маленькой буквы и без точки в конце: он часто попадает в середину другого сообщения — как во второй строке вывода на следующей странице.


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

Что видите Что это значит Что делать
assignment mismatch: 1 variable but parseQty returns 2 values функция с ошибкой вызвана как обычная qty, err := parseQty(…)
declared and not used: err ошибку получили и ни разу не проверили if err != nil { … } сразу под вызовом
cannot use errors.New(…) (value of interface type error) as int value in return statement значения в return стоят в порядке, обратном заголовку сначала результат, ошибка последней
invalid operation: err != 0 (mismatched types error and untyped int) ошибку сравнивают с нулём err != nil
cannot convert "" (untyped string constant) to type interface{Error() string} ошибку сравнивают с пустой строкой err != nil
undefined: errors пакет не подключён "errors" в import
функция вернула ошибку, а программа посчитала результат результат взяли, не посмотрев на ошибку при ошибке результат не использовать
ошибка «теряется»: в main всё хорошо, хотя внутри было плохо функция напечатала сообщение вместо return ошибки возвращать ошибку, решает вызывающий

Дальше: что делает с ошибкой тот, кто вызвал функцию — course next