Обработка ошибок в Go: try/catch и не только — продвинутый уровень

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Обработка ошибок в Go: try/catch и не только — продвинутый уровень

В отличие от многих современных языков (Java, Python, JavaScript), Go не имеет традиционных конструкций try/catch/finally. Разработчики, пришедшие из других экосистем, часто воспринимают это как недостаток. Однако философия Go предлагает более явный и предсказуемый способ работы с ошибками. В этой статье мы разберем продвинутые техники обработки ошибок в Go: от базовых принципов до создания гибких систем с panic/recover, обертками ошибок и пользовательскими типами.

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

Основная причина — простота и читаемость кода. В Go ошибка — это просто значение (error), которое функция возвращает как последний результат. Разработчик явно проверяет это значение, а не полагается на скрытый поток исключений. Это делает поток выполнения программы очевидным.

Стандартный паттерн выглядит так:

package main

import (
    "fmt"
    "os"
)

func readFile(name string) ([]byte, error) {
    data, err := os.ReadFile(name)
    if err != nil {
        return nil, fmt.Errorf("не удалось прочитать файл %s: %w", name, err)
    }
    return data, nil
}

func main() {
    data, err := readFile("config.json")
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }
    fmt.Println("Данные:", string(data))
}

Здесь каждая потенциальная ошибка обрабатывается явно. Это не «шум», а гарантия того, что программист подумал о возможном сбое.

2. Продвинутые техники: обертки и цепочки ошибок

Go 1.13 ввел поддержку обертывания ошибок через %w в fmt.Errorf. Это позволяет создавать цепочки ошибок, сохраняя исходную причину. Для распаковки таких цепочек используются функции errors.Is и errors.As.

Пример обертывания и проверки:

package main

import (
    "errors"
    "fmt"
)

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

func findUser(id int) (string, error) {
    if id != 42 {
        return "", fmt.Errorf("поиск пользователя %d: %w", id, ErrNotFound)
    }
    return "Alice", nil
}

func main() {
    _, err := findUser(100)
    if errors.Is(err, ErrNotFound) {
        fmt.Println("Ошибка: запись отсутствует. Подробнее:", err)
    } else {
        fmt.Println("Неизвестная ошибка:", err)
    }
}

Использование errors.As для типизированных ошибок:

package main

import (
    "errors"
    "fmt"
)

type ValidationError struct {
    Field string
    Msg   string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("ошибка валидации поля %s: %s", e.Field, e.Msg)
}

func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{Field: "age", Msg: "возраст не может быть отрицательным"}
    }
    return nil
}

func main() {
    err := validateAge(-5)
    var ve *ValidationError
    if errors.As(err, &ve) {
        fmt.Printf("Поле: %s, Сообщение: %s\n", ve.Field, ve.Msg)
    } else {
        fmt.Println("Неизвестная ошибка:", err)
    }
}

3. Panic и Recover: аварийное восстановление

Хотя panic в Go — это аналог исключения, его следует использовать только для truly exceptional ситуаций (например, логическая ошибка программиста, деление на ноль, выход за границы массива). recover позволяет перехватить панику и восстановить выполнение.

Важно: recover работает только внутри отложенной функции (defer). Типичный сценарий — middleware в HTTP-сервере или защита горутины.

Пример защищенного запуска горутины:

package main

import (
    "fmt"
    "time"
)

func safeGoroutine(fn func()) {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("Восстановление после паники:", r)
            }
        }()
        fn()
    }()
}

func main() {
    safeGoroutine(func() {
        panic("что-то пошло не так")
    })
    time.Sleep(100 * time.Millisecond)
    fmt.Println("Программа продолжает работу")
}

Важное предостережение: Не используйте panic/recover как замену обычной обработки ошибок. Это нарушает философию Go и делает код менее предсказуемым.

4. Создание собственных типов ошибок и паттерн Sentinel

Паттерн «Sentinel errors» (ошибки-маяки) — это объявление глобальных переменных-ошибок, которые можно сравнивать через == или errors.Is. Он широко используется в стандартной библиотеке (io.EOF, sql.ErrNoRows).

Пример реализация кастомного типа ошибки с дополнительными данными:

package main

import (
    "fmt"
)

type HTTPError struct {
    Code    int
    Message string
}

func (e *HTTPError) Error() string {
    return fmt.Sprintf("HTTP %d: %s", e.Code, e.Message)
}

func (e *HTTPError) StatusCode() int {
    return e.Code
}

func fetchData(url string) (string, error) {
    if url == "" {
        return "", &HTTPError{Code: 400, Message: "URL не может быть пустым"}
    }
    return "response data", nil
}

func main() {
    _, err := fetchData("")
    if err != nil {
        if httpErr, ok := err.(*HTTPError); ok {
            fmt.Printf("Код ошибки: %d, Сообщение: %s\n", httpErr.Code, httpErr.Message)
        } else {
            fmt.Println("Неизвестная ошибка:", err)
        }
    }
}

Этот подход позволяет передавать дополнительный контекст (код ответа, поле формы, ID записи) вместе с ошибкой, не теряя совместимости со стандартным интерфейсом error.

5. Best Practices для обработки ошибок в Go

  • Всегда проверяйте ошибки. Не игнорируйте возвращаемые значения с помощью _.
  • Обертывайте ошибки с контекстом. Используйте fmt.Errorf("что-то пошло не так: %w", err).
  • Не злоупотребляйте panic. Исп

Блоги

Рекомендаций по книгам