JavaRush /Курсы /Go SELF /error как интерфейс — nil/не nil

error как интерфейс — nil/не nil

Go SELF
16 уровень , 0 лекция
Открыта

1. Ошибки в Go: значения и интерфейс error

Если вы приходите в Go после языков, где есть исключения (exceptions), первый культурный шок обычно выглядит так: «Почему мне нужно писать проверку ошибки после каждого второго вызова?». Это чувство нормально: мозг привык, что ошибка — это что-то сверхъестественное, а в Go она максимально приземлённая — буквально переменная, которую можно присваивать, возвращать из функций и печатать.

Один из ключевых лозунгов Go‑мира звучит так: errors are values — «ошибки являются значениями». Именно поэтому Go поощряет явную проверку err, а не «вылет» куда-то вверх по стеку, где потом кто-то где-то попытается «поймать» исключение. Такой стиль делает поток управления более предсказуемым: видно, где программа может “споткнуться”, и видно, что именно произойдёт в этом месте.

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

Тип error: что это такое на самом деле

Сейчас будет важный момент: error — это тип, причём не просто тип, а интерфейсный тип. В стандартной библиотеке Go он определён примерно так:

type error interface {
    Error() string
}

Если вы ещё не изучали интерфейсы «системно» — всё равно не страшно. Для сегодняшней лекции достаточно понимать интерфейс очень прагматично: интерфейс — это договор “у значения есть такие-то методы”. Здесь договор минимальный: если у значения есть метод Error(), возвращающий string, то такое значение можно использовать как error.

И вот тут начинаются хорошие новости: это объясняет сразу несколько странностей, которые новичка обычно пугают.

  • Во-первых, error — не строка. Он может давать строку через Error(), но сам по себе он не string.
  • Во-вторых, ошибки бывают разных “внутренних” (конкретных) типов: strconv.Atoi возвращает один конкретный тип ошибки, fmt.Scan может вернуть другой, и так далее. Но снаружи мы держим их в переменной типа error, потому что нас устраивает общий договор: “можно получить текст”.
  • В-третьих, именно поэтому fmt умеет красиво печатать ошибки: он знает, что у ошибки есть метод Error(), и может его вызвать.

Чтобы увидеть идею “интерфейс = договор методов” на пальцах, вот крошечная демонстрация:

package main

import "fmt"

type myError struct{}

func (myError) Error() string { // реализуем договор error
	return "something went wrong"
}

func main() {
	var err error = myError{}
	fmt.Println(err) // something went wrong
}

Здесь ноль магии: у myError есть метод Error(), значит его можно положить в переменную типа error.

nil как признак успеха: zero value для error

Теперь про самое прикладное правило Go‑кода: nil в error означает успех. А не‑nil означает, что произошла ошибка. Это не “особый оператор”, а договорённость, на которой держится весь стиль Go.

Важно, что zero value (значение по умолчанию) для переменной типа error — это как раз nil. То есть если вы объявили var err error, то “по умолчанию” у вас “ошибки нет”.

package main

import "fmt"

func main() {
	var err error // zero value для error — nil
	fmt.Println(err == nil) // true
	fmt.Println(err)        // <nil>
}

Обратите внимание на вторую печать: fmt.Println(err) выводит <nil>. Это не строка “nil”, а специальный формат вывода nil‑значения.

Теперь свяжем это с тем, что мы видели раньше в strconv.Atoi. Если парсинг удался, err будет nil. Если не удался — err будет не nil, и внутри будет конкретный объект ошибки.

package main

import (
	"fmt"
	"strconv"
)

func main() {
	n, err := strconv.Atoi("42")
	fmt.Println(n)          // 42
	fmt.Println(err == nil) // true
}

А теперь — провокация:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	n, err := strconv.Atoi("4x")
	fmt.Println(n)   // 0
	fmt.Println(err) // strconv.Atoi: parsing "4x": invalid syntax
}

Здесь видно сразу два факта.

  • Первый: при ошибке n стал равен 0 — это zero value для int.
  • Второй: ошибка содержит текст, который объясняет, что случилось.

И это подводит нас к дисциплине: если err != nil, результат (n) нельзя считать валидным, даже если он выглядит «похожим на правду». Сегодня может быть 0, завтра (в других функциях) может быть пустая строка, nil‑слайс и так далее.

Как fmt печатает ошибки

Очень частый вопрос новичка: «Почему fmt.Println(err) печатает нормальный текст, хотя err — это не строка?». Правильный ответ: fmt печатает ошибки, вызывая метод Error() у значения ошибки.

То есть когда вы пишете fmt.Println(err), это концептуально похоже на “распечатай err.Error()”, только fmt ещё умеет аккуратно обработать случай err == nil.

package main

import (
	"fmt"
	"strconv"
)

func main() {
	_, err := strconv.Atoi("x")

	fmt.Printf("as %%v: %v\n", err) // as %v: strconv.Atoi: parsing "x": invalid syntax
	fmt.Println("as Println:", err) // as Println: strconv.Atoi: parsing "x": invalid syntax
}

Оба варианта используют одну и ту же идею: fmt видит значение типа error и понимает, как превратить его в текст.

Ещё один полезный приём для диагностики: печать типа через %T. Это особенно полезно, когда вы хотите понять, какая именно ошибка внутри интерфейса error:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	_, err := strconv.Atoi("x")
	fmt.Printf("type: %T\n", err) // type: *strconv.NumError
}

Вывод *strconv.NumError — это подсказка: “внутри error лежит конкретная структура ошибки из пакета strconv”. Полезно помнить, что error — это оболочка (интерфейс), а внутри может быть что угодно, лишь бы был метод Error().

err.Error() и почему мы не анализируем текст ошибки

Когда вы видите err.Error(), это не “магия”, а обычный вызов метода через точку: “у объекта err вызови метод Error”. Строго по договору error этот метод возвращает string.

Иногда вам действительно нужна именно строка, например, чтобы склеить её с каким‑то своим сообщением или вывести пользователю только текст. Тогда err.Error() уместен.

package main

import (
	"fmt"
	"strconv"
)

func main() {
	_, err := strconv.Atoi("x")
	if err != nil {
		fmt.Println("message:", err.Error()) // message: strconv.Atoi: parsing "x": invalid syntax
	}
}

Но здесь есть важная дисциплина: мы не принимаем решения, анализируя текст ошибки. То есть плохая идея — делать так:

// пример того, как делать НЕ надо (не копируйте)
if err != nil && strings.Contains(err.Error(), "invalid syntax") {
    ...
}

Почему плохо? Потому что текст ошибки — это сообщение для человека (лог/вывод), а не стабильный API для программы. Сегодня оно одно, завтра другое.

Правильная база сегодняшнего дня простая: решение “успех/ошибка” мы делаем по err == nil или err != nil, а не по строкам.

strconv.Atoi как фабрика реальных error

Слово “интерфейс” иногда звучит слишком абстрактно, поэтому полезно превратить его в самый знакомый нам источник ошибок — strconv.Atoi. Давайте посмотрим на два запуска: “успех” и “ошибка”, и в обоих случаях напечатаем и тип, и значение err.

package main

import (
	"fmt"
	"strconv"
)

func main() {
	_, err1 := strconv.Atoi("10")
	fmt.Printf("err1: %v (%T)\n", err1, err1) // err1: <nil> (<nil>)

	_, err2 := strconv.Atoi("1O") // буква O вместо нуля
	fmt.Printf("err2: %v (%T)\n", err2, err2) // err2: ... (*strconv.NumError)
}

Обратите внимание: у err1 тип печатается как <nil>. Это нормально: интерфейсная переменная error не содержит конкретного значения, и поэтому %T показывает <nil>.

У err2 появляется конкретный тип *strconv.NumError. Это и есть “конкретная ошибка”, спрятанная за интерфейсом.

И вот почему в Go так удобно жить: вам не нужно в каждом месте знать, что там *strconv.NumError. Вам достаточно знать: если err != nil, операция не удалась, и ошибку можно напечатать или вернуть дальше.

2. Мини-приложение «Учёт расходов»

Сейчас соберём небольшой учебный пример, который будет развиваться дальше в рамках дня про ошибки. Идея максимально бытовая: у нас есть список расходов, каждый расход — это категория и сумма. Мы хотим посчитать общий итог и сумму по категориям. А ещё мы хотим, чтобы программа не делала вид, что всё хорошо, если пользователь ввёл “сто евро” вместо 100.

Формат ввода сделаем простым: сначала число n (сколько записей), потом n пар категория сумма. Сумма приходит как строка, а мы парсим её через strconv.Atoi, чтобы получить классическую ошибку (а не “молчаливое превращение в ноль”).

Для начала напишем маленькую функцию чтения одного токена. Это позволит нам в одном месте увидеть, что даже fmt.Scan может вернуть error.

package main

import "fmt"

func readToken() (string, error) {
	var s string
	_, err := fmt.Scan(&s)
	return s, err
}

Теперь сделаем функцию чтения суммы: она читает строковый токен и пытается превратить его в int.

package main

import (
	"strconv"
)

func readAmount() (int, error) {
	token, err := readToken()
	if err != nil {
		return 0, err
	}
	return strconv.Atoi(token)
}

Здесь важен стиль возврата: если возникла ошибка, мы возвращаем 0 (zero value для int) и саму ошибку. Мы пока не улучшаем текст ошибки — сегодня нам важно понять, что это просто значение и оно либо nil, либо нет.

Теперь соберём main, который читает n, затем n записей, и складывает суммы по категориям в map[string]int.

package main

import (
	"fmt"
)

func main() {
	n, err := readAmount()
	if err != nil {
		fmt.Println("failed to read n:", err)
		return
	}

	totals := make(map[string]int)

	for i := 0; i < n; i++ {
		cat, err := readToken()
		if err != nil {
			fmt.Println("failed to read category:", err)
			return
		}

		amount, err := readAmount()
		if err != nil {
			fmt.Println("failed to read amount:", err)
			return
		}

		totals[cat] = totals[cat] + amount
	}

	fmt.Println("totals:", totals)
}

Эта версия нарочно простая: при первой же ошибке мы печатаем сообщение и выходим. С точки зрения сегодняшней темы это идеально, потому что показывает главный контракт: пока err == nil, мы продолжаем; как только err != nil, мы выбираем ветку “ошибка”.

Чтобы увидеть, что программа ведёт себя предсказуемо, представьте такой ввод:

3
food 120
taxi 300
food x

На третьей записи strconv.Atoi("x") вернёт не‑nil ошибку, amount станет 0, и мы не будем добавлять “нулевой расход” как будто всё нормально — мы остановимся и покажем пользователю проблему.

Заметьте ещё одну важную вещь про map: если в категории ещё нет суммы, totals[cat] даёт 0 (zero value для int). Поэтому выражение totals[cat] = totals[cat] + amount работает и для первой записи, и для последующих.

Небольшая схема потока управления у нас такая (и это хороший шаблон на будущее):

flowchart TD
    A[Прочитать токен/число] --> B{err == nil?}
    B -- да --> C[Использовать значение]
    C --> A
    B -- нет --> D[Показать ошибку]
    D --> E[Остановиться]

3. Типичные ошибки при первых шагах с error

Ошибка №1: воспринимать error как строку и проверять “пустая / не пустая”.
Иногда хочется сделать что-то вроде if err != "", потому что “ну ошибка же текст”. Но error — это отдельный тип, не string. Проверка корректна только через err == nil или err != nil. Если вам нужен текст, его получают через err.Error(), но это уже последующий шаг, а не способ определить факт ошибки.

Ошибка №2: печатать ошибку и продолжать вычисления с невалидным результатом.
Ситуация: n, err := strconv.Atoi(...), затем вы печатаете ошибку, но всё равно используете n. Проблема в том, что при ошибке n почти всегда будет zero value (для int это 0), и вы тихо получите неправильный итог. Если вы решили, что ошибка критична, то после сообщения должен быть явный переход потока управления (например, return из main).

Ошибка №3: путать “успех” и “ошибка” в условии.
Бывает написать if err == nil { ... обработка ошибки ... }. Держите в голове правило: err != nil — ветка ошибки.

Ошибка №4: не понимать, что fmt.Println(err) не просто “печатает переменную”.
Текст появляется потому, что fmt вызывает метод Error() у ошибки. Это полезно помнить, чтобы не ожидать, что error “сам по себе строка”.

Ошибка №5: пытаться принимать решения по тексту err.Error().
Соблазн велик: текст же понятный. Но он не предназначен для логики ветвления. Привыкайте к дисциплине: “есть ошибка или нет ошибки” решаем через nil/не‑nil. Текст — для пользователя/лога, а не для алгоритма.

1
Задача
Go SELF, 16 уровень, 0 лекция
Недоступна
Датчик ошибок
Датчик ошибок
1
Задача
Go SELF, 16 уровень, 0 лекция
Недоступна
Своя тревога
Своя тревога
1
Задача
Go SELF, 16 уровень, 0 лекция
Недоступна
Пропуск числа
Пропуск числа
1
Задача
Go SELF, 16 уровень, 0 лекция
Недоступна
Касса расходов
Касса расходов
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ