Обработка ошибок в 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. Исп