Интерфейс error и его идиомы

Разбираем, почему в Go нет try/catch и как правильно работать с ошибками — это первый вопрос почти на любом Go-собеседовании.

error — это встроенный интерфейс Go всего с одним методом Error() string. Любой тип, у которого есть такой метод, можно использовать как ошибку.

Что выведет этот код?

Начнём с вопроса, который любят задавать разработчикам, приходящим в Go из Java, Python или C++.

package main

import (
	"errors"
	"fmt"
)

func delenie(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("деление на ноль")
	}
	return a / b, nil
}

func main() {
	result, err := delenie(10, 0)
	if err != nil {
		fmt.Println("Ошибка:", err)
		return
	}
	fmt.Println("Результат:", result)
}

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

Вывод:

Ошибка: деление на ноль

Почему Go отказался от исключений

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

Авторы Go решили сделать наоборот: ошибка — это обычное значение, которое функция возвращает явно, вторым (или последним) результатом. Раз ошибка — значение, то её видно прямо в сигнатуре функции: func delenie(a, b int) (int, error) сразу говорит читателю «эта функция может завершиться неудачно, и вот тип, в котором придёт причина». Ничего не спрятано, ничего не «вылетает» из середины кода незаметно.

Идиома if err != nil

Отсюда растёт самая узнаваемая идиома Go — проверка if err != nil буквально после каждого вызова, который может вернуть ошибку. Да, это выглядит многословно по сравнению с одним try/catch на весь блок кода. Но у этой многословности есть цель: ты обрабатываешь (или осознанно пробрасываешь) ошибку именно там, где она возникла, пока у тебя ещё есть весь контекст произошедшего — какие были аргументы, что за операция не удалась.

func obrabotatFayl(imya string) error {
	data, err := chitatFayl(imya)
	if err != nil {
		return fmt.Errorf("не удалось прочитать файл: %w", err)
	}

	result, err := parsitData(data)
	if err != nil {
		return fmt.Errorf("не удалось разобрать данные: %w", err)
	}

	fmt.Println("Разобрано байт:", len(result))
	return nil
}

Здесь видно закономерность: получил ошибку — сразу решил, что с ней делать. В большинстве случаев ответ один — вернуть её выше по цепочке вызовов, но добавив немного контекста о том, что именно происходило в этой функции.

Обёртывание ошибок через fmt.Errorf

Именно для добавления контекста и существует специальный глагол %w внутри fmt.Errorf — он «оборачивает» исходную ошибку в новую, более подробную, но не теряет оригинал. Это критически важное отличие от простой конкатенации строк вроде errors.New("не удалось прочитать файл: " + err.Error()): при обычной конкатенации получается просто новая строка, а исходная ошибка как объект теряется безвозвратно. При использовании %w исходная ошибка остаётся «внутри» новой, и её можно потом достать обратно с помощью функций errors.Is и errors.As из стандартного пакета errors.

var ErrNeNayden = errors.New("запись не найдена")

func naytiPolzovatelya(id int) error {
	return fmt.Errorf("поиск пользователя %d: %w", id, ErrNeNayden)
}

func main() {
	err := naytiPolzovatelya(42)
	if errors.Is(err, ErrNeNayden) {
		fmt.Println("Пользователь действительно не найден")
	}
	fmt.Println(err)
}

Вывод:

Пользователь действительно не найден
поиск пользователя 42: запись не найдена

Обрати внимание: errors.Is смогла «докопаться» до ErrNeNayden, хотя напрямую сравнивалась вовсе не она, а обёрнутая версия — именно за счёт цепочки оборачивания через %w. Если бы мы использовали обычный %v вместо %w, такая проверка бы уже не сработала: строка выглядела бы одинаково, но связь между ошибками на уровне объектов пропала бы.

Частые ошибки на собеседовании

Первая и самая частая ошибка — путать %v и %w в fmt.Errorf. Кандидаты часто вообще не знают о существовании %w и оборачивают ошибки через %v, из-за чего теряется возможность проверить исходную причину через errors.Is. Вторая ошибка — сравнивать ошибки через обычное == вместо errors.Is, что ломается, как только в цепочке появляется хоть одно оборачивание. Третья, чуть более тонкая ошибка — игнорировать возвращённую ошибку вовсе, присваивая её символу подчёркивания _ «чтобы код скомпилировался» — на собеседовании это почти всегда красный флаг, потому что в реальном коде так теряются важные сигналы о сбоях.

Итоги

  • В Go нет исключений — ошибка возвращается как обычное значение типа error, и это видно прямо в сигнатуре функции.
  • Идиома if err != nil заставляет обрабатывать ошибку сразу после вызова, пока есть весь контекст.
  • fmt.Errorf с глаголом %w оборачивает ошибку, сохраняя связь с оригиналом для дальнейшей проверки через errors.Is и errors.As.
  • Игнорировать ошибку через _ — плохая практика, на собеседовании за это почти всегда снимут баллы.
Проверьте себя
1. Как Go сигнализирует об ошибке в функции, в отличие от Java или Python?
AС помощью блока try/catch, как и в других языках
BВозвращает ошибку как обычное значение типа error, обычно последним результатом функции
CАвтоматически останавливает программу при любой ошибке
DЗаписывает ошибку в специальный глобальный лог-файл
2. В чём разница между %v и %w внутри fmt.Errorf?
AРазницы нет, это просто два синонима одного и того же
B%w сохраняет связь с исходной ошибкой для errors.Is и errors.As, а %v просто вставляет текст
C%w работает только с числовыми ошибками
D%v быстрее по производительности, поэтому его рекомендуют использовать всегда