← Все этапы
04 · ПАКЕТЫ И НАДЁЖНОСТЬУрок 23 из 3325 минут

Ошибки, обёртки и проверка

Возвращаем ошибки явно, добавляем контекст и проверяем их через errors.Is и errors.As.

Ошибка — часть результата

В Go функция, которая может не справиться, обычно возвращает ошибку последним результатом. Успех обозначает nil; любое ненулевое значение нужно проверить до использования остальных результатов. Текст ошибки можно создать через errors.New, а контекст добавить форматированием.

При оборачивании используй verb %w: он сохраняет исходную ошибку внутри цепочки. errors.Is(err, target) отвечает, есть ли в цепочке конкретная ошибка или sentinel. errors.As(err, &target) ищет в цепочке ошибку нужного типа и при успехе записывает её в переменную. В As передают указатель на переменную подходящего типа.

Для собственного типа ошибки реализуй метод Error() string с получателем. Структурированная ошибка полезна, когда вызывающему коду нужны данные вроде неверного значения; для ветвления по стабильной категории подойдёт sentinel.

Пример

package main

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

var errDivisionByZero = errors.New("division by zero")

type portRangeError struct {
	port int
}

// Error describes a port that is outside the accepted range.
func (err *portRangeError) Error() string {
	return fmt.Sprintf("port %d is outside 1..65535", err.port)
}

func parsePort(input string) (int, error) {
	port, err := strconv.Atoi(input)
	if err != nil {
		return 0, fmt.Errorf("parse port %q: %w", input, err)
	}
	if port < 1 || port > 65535 {
		return 0, fmt.Errorf("validate port: %w", &portRangeError{port: port})
	}
	return port, nil
}

func divide(total, count int) (int, error) {
	if count == 0 {
		return 0, fmt.Errorf("divide %d by %d: %w", total, count, errDivisionByZero)
	}
	return total / count, nil
}

func main() {
	port, err := parsePort("70000")
	if err != nil {
		var rangeErr *portRangeError
		if errors.As(err, &rangeErr) {
			fmt.Printf("неподходящий порт: %d\n", rangeErr.port)
		} else {
			fmt.Printf("ошибка настройки: %v\n", err)
		}
	} else {
		fmt.Printf("порт: %d\n", port)
	}

	_, err = divide(10, 0)
	if errors.Is(err, errDivisionByZero) {
		fmt.Printf("деление отклонено: %v\n", err)
	}
}

Порт 70000 приводит к пользовательской ошибке с полем port; обёртка добавляет контекст, а errors.As всё ещё находит исходный тип. Обработка этой ошибки не прерывает программу, поэтому в том же запуске видна проверка ошибки деления через errors.Is.

Не сравнивай произвольные ошибки по строке и не создавай заново один и тот же sentinel внутри каждой функции. Возвращай полезный контекст, сохраняй причинную ошибку через %w, а вызывающий код пусть выбирает действие по типу или категории.

Практика

Попробуйте сами

Напиши функцию parseAge(input string) (int, error). При неверном числе она должна оборачивать ошибку strconv.Atoi с контекстом через %w; при числе меньше 0 или больше 120 возвращать обёрнутую пользовательскую ошибку ageRangeError с исходным значением. В main обработай корректный возраст, строку «много» через errors.As к *strconv.NumError и значение 150 через errors.As к *ageRangeError.

Проверка результата

Как понять, что получилось

Для «42» функция возвращает 42 и nil. Для «много» ошибка позволяет errors.As найти *strconv.NumError. Для «150» находится *ageRangeError с полем значения 150. Обе ошибки содержат контекст операции и сохраняют исходную ошибку или тип через %w.

Ошибки, обёртки и проверка | Go | WebSchool · Go