Интерфейс 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.- Игнорировать ошибку через
_— плохая практика, на собеседовании за это почти всегда снимут баллы.