Модуль
1Ошибки: основы← вы здесь2Оборачивание ошибок3Свои типы ошибок4panic и recover
Урок 1~12 минут

Ошибки: основы

Почему в Go нет try/catch

Первое что удивляет новичков в Go — отсутствие исключений. В Java, Python, C# ошибки — это исключения, которые «бросаются» и «ловятся». В Go ошибки — это обычные значения, которые возвращаются из функций.

Почему? Потому что исключения создают невидимые пути выполнения. Когда ты видишь try { doSomething() }, ты не знаешь из какой именно строки может прилететь исключение. В Go всё явно: функция либо вернула результат, либо вернула ошибку — и ты обязан это проверить.

go
// В Java (скрытые пути):
try {
    result = riskyOperation()
    anotherRiskyCall()
    yetAnother()
} catch (Exception e) { }
 
// В Go (явные пути):
result, err := riskyOperation()
if err != nil {
    return err
}

Да, это многословнее. Зато читая код ты точно знаешь — вот здесь может быть ошибка, и вот что с ней делают.


Интерфейс error

В Go ошибка — это любой тип, реализующий встроенный интерфейс:

go
type error interface {
    Error() string
}

Один метод. Всё. Если твой тип имеет метод Error() string — он автоматически является ошибкой. Никаких базовых классов, никакого наследования.

go
// Пример: создаём свою ошибку
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

Для простых ошибок не нужно создавать свои типы — есть два стандартных способа:

go
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 есть чёткая конвенция: ошибка всегда последнее возвращаемое значение:

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) { ... }

Это позволяет использовать единый паттерн проверки:

go
data, err := ReadFile("config.json")
if err != nil {
    // обрабатываем ошибку
    return nil, fmt.Errorf("не удалось прочитать конфиг: %w", err)
}
// используем data

Проверка ошибок: if err != nil

Главный паттерн в Go:

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
}

Три стратегии при получении ошибки:

  1. Вернуть выше — добавив контекст через fmt.Errorf
  2. Обработать — исправить ситуацию и продолжить
  3. Залогировать и продолжить — если ошибка некритична

Типичная ошибка джуна: игнорирование

go
// ПЛОХО: ошибка молча игнорируется
data, _ := ioutil.ReadFile("config.json")
json.Unmarshal(data, &config)
 
// Что произойдёт если файла нет?
// data будет nil, Unmarshal вернёт ошибку, config останется пустым.
// Программа продолжит работу с пустым конфигом — и упадёт позже,
// в совершенно другом месте, с непонятной ошибкой.
go
// ХОРОШО: всегда проверяй ошибки
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 позволяет именовать возвращаемые значения — это полезно для документации:

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-паттернов:

go
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)
}
В Go ошибка — это просто значение с методом Error() string. Нет try/catch — ошибки возвращаются как обычные значения и проверяются через if err != nil.

В Go error — встроенный интерфейс с одним методом:

type error interface { Error() string }
Значение ошибки:NIL
var err error = nil // err == nil => true // типа: <nil>
if err != nil { ... } // блок НЕ выполняется
🎯
Миссия 1 из 4
Какой интерфейс реализует любой тип ошибки в Go?