Ошибки: основы
Почему в Go нет try/catch
Первое что удивляет новичков в Go — отсутствие исключений. В Java, Python, C# ошибки — это исключения, которые «бросаются» и «ловятся». В Go ошибки — это обычные значения, которые возвращаются из функций.
Почему? Потому что исключения создают невидимые пути выполнения. Когда ты видишь try { doSomething() }, ты не знаешь из какой именно строки может прилететь исключение. В Go всё явно: функция либо вернула результат, либо вернула ошибку — и ты обязан это проверить.
// В Java (скрытые пути):
try {
result = riskyOperation()
anotherRiskyCall()
yetAnother()
} catch (Exception e) { }
// В Go (явные пути):
result, err := riskyOperation()
if err != nil {
return err
}Да, это многословнее. Зато читая код ты точно знаешь — вот здесь может быть ошибка, и вот что с ней делают.
Интерфейс error
В Go ошибка — это любой тип, реализующий встроенный интерфейс:
type error interface {
Error() string
}Один метод. Всё. Если твой тип имеет метод Error() string — он автоматически является ошибкой. Никаких базовых классов, никакого наследования.
// Пример: создаём свою ошибку
type MyError struct {
Code int
Message string
}
func (e *MyError) Error() string {
return fmt.Sprintf("код %d: %s", e.Code, e.Message)
}
// Использование:
var err error = &MyError{Code: 404, Message: "не найдено"}
fmt.Println(err) // код 404: не найденоerrors.New и fmt.Errorf
Для простых ошибок не нужно создавать свои типы — есть два стандартных способа:
import (
"errors"
"fmt"
)
// errors.New — статическое сообщение
err1 := errors.New("файл не найден")
// fmt.Errorf — с форматированием (как fmt.Sprintf)
name := "config.json"
err2 := fmt.Errorf("не удалось открыть файл %q", name)
fmt.Println(err1) // файл не найден
fmt.Println(err2) // не удалось открыть файл "config.json"Когда что использовать:
errors.New— когда сообщение статическое и не зависит от контекстаfmt.Errorf— когда нужно включить динамические данные (имя файла, ID пользователя и т.д.)
Конвенция возврата ошибок
В Go есть чёткая конвенция: ошибка всегда последнее возвращаемое значение:
// Правильно:
func ReadFile(path string) ([]byte, error) { ... }
func GetUser(id int) (*User, error) { ... }
func ParseConfig(data []byte) (*Config, error) { ... }
// Неправильно (нарушает конвенцию):
func ReadFile(path string) (error, []byte) { ... }Это позволяет использовать единый паттерн проверки:
data, err := ReadFile("config.json")
if err != nil {
// обрабатываем ошибку
return nil, fmt.Errorf("не удалось прочитать конфиг: %w", err)
}
// используем dataПроверка ошибок: if err != nil
Главный паттерн в Go:
func processUser(id int) error {
user, err := db.GetUser(id)
if err != nil {
return fmt.Errorf("processUser: %w", err)
}
if err := user.Validate(); err != nil {
return fmt.Errorf("невалидный пользователь %d: %w", id, err)
}
if err := sendWelcomeEmail(user.Email); err != nil {
// Некритичная ошибка — логируем, но не возвращаем
log.Printf("не удалось отправить email: %v", err)
}
return nil
}Три стратегии при получении ошибки:
- Вернуть выше — добавив контекст через
fmt.Errorf - Обработать — исправить ситуацию и продолжить
- Залогировать и продолжить — если ошибка некритична
Типичная ошибка джуна: игнорирование
// ПЛОХО: ошибка молча игнорируется
data, _ := ioutil.ReadFile("config.json")
json.Unmarshal(data, &config)
// Что произойдёт если файла нет?
// data будет nil, Unmarshal вернёт ошибку, config останется пустым.
// Программа продолжит работу с пустым конфигом — и упадёт позже,
// в совершенно другом месте, с непонятной ошибкой.// ХОРОШО: всегда проверяй ошибки
data, err := os.ReadFile("config.json")
if err != nil {
return fmt.Errorf("не удалось прочитать конфиг: %w", err)
}
if err := json.Unmarshal(data, &config); err != nil {
return fmt.Errorf("невалидный JSON в конфиге: %w", err)
}Линтер errcheck находит проигнорированные ошибки. В реальных проектах его включают в CI.
Именованные возвращаемые значения
Go позволяет именовать возвращаемые значения — это полезно для документации:
func divide(a, b float64) (result float64, err error) {
if b == 0 {
err = errors.New("деление на ноль")
return // naked return — возвращает result=0, err=...
}
result = a / b
return
}Используй осторожно: в длинных функциях naked return затрудняет чтение. Лучше всего подходит для коротких функций или defer-паттернов:
func fetchData(url string) (data []byte, err error) {
resp, err := http.Get(url)
if err != nil {
return // data=nil, err=ошибка
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}