Обработка ошибок в Go: философия и практика
В отличие от многих современных языков, Go (Golang) не использует классическую конструкцию try/catch. Вместо этого разработчики Go придерживаются подхода «ошибки как значения» (errors as values). Это делает код более явным, предсказуемым и безопасным. В этой статье мы разберем все аспекты обработки ошибок в Go: от базовых паттернов до продвинутых техник с defer, panic и recover.
1. Основы: ошибки — это просто значения
В Go ошибка — это интерфейс error с единственным методом Error() string. Любая функция, которая может завершиться неудачей, возвращает ошибку как последнее возвращаемое значение.
package main
import (
"errors"
"fmt"
)
// Функция деления с возвратом ошибки
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("деление на ноль невозможно")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Ошибка:", err)
return
}
fmt.Println("Результат:", result)
}
Ключевой момент: проверка if err != nil — это стандартный способ обработки ошибок в Go. Нет скрытых исключений, которые могут «вылететь» неожиданно.
2. Создание и кастомизация ошибок
Go предоставляет несколько способов создания ошибок:
errors.New()— простая текстовая ошибка.fmt.Errorf()— ошибка с форматированием (поддерживает%wдля wrapping).- Пользовательские типы — для передачи дополнительного контекста.
Пользовательская ошибка с дополнительными данными
type DivisionError struct {
Dividend float64
Divisor float64
Message string
}
func (e *DivisionError) Error() string {
return fmt.Sprintf("%s: %f / %f", e.Message, e.Dividend, e.Divisor)
}
func safeDivide(a, b float64) (float64, error) {
if b == 0 {
return 0, &DivisionError{
Dividend: a,
Divisor: b,
Message: "деление на ноль",
}
}
return a / b, nil
}
func main() {
_, err := safeDivide(10, 0)
if err != nil {
// Type assertion для получения дополнительных полей
var divErr *DivisionError
if errors.As(err, &divErr) {
fmt.Printf("Ошибка с %f на %f: %s\n", divErr.Dividend, divErr.Divisor, divErr.Message)
}
}
}
Использование errors.As() позволяет проверить, является ли ошибка определенным типом, и извлечь из нее данные. Это аналог instanceof в других языках.
3. Паттерны обработки ошибок: от простого к сложному
В Go существует несколько устоявшихся паттернов работы с ошибками:
3.1. Sentinel errors (ошибки-маркеры)
Это предопределенные ошибки, которые можно сравнивать с помощью ==. Пример: io.EOF.
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("элемент не найден")
func findUser(id int) (string, error) {
if id != 1 {
return "", ErrNotFound
}
return "Alice", nil
}
func main() {
_, err := findUser(2)
if errors.Is(err, ErrNotFound) {
fmt.Println("Пользователь не найден, создаем нового...")
}
}
3.2. Wrapping ошибок (обертывание)
Начиная с Go 1.13, можно оборачивать ошибки с помощью %w в fmt.Errorf. Это сохраняет цепочку ошибок.
func readConfig(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("чтение конфига %s: %w", path, err)
}
// ... обработка data
return nil
}
func main() {
err := readConfig("/nonexistent/config.json")
if err != nil {
// Проверка, есть ли в цепочке os.ErrNotExist
if errors.Is(err, os.ErrNotExist) {
fmt.Println("Файл не существует:", err)
} else {
fmt.Println("Другая ошибка:", err)
}
}
}
3.3. Паника и восстановление (panic/recover)
Хотя panic в Go не является заменой try/catch, он используется для действительно исключительных ситуаций (например, ошибки программиста). recover позволяет перехватить панику.
func safeCall(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("паника: %v", r)
}
}()
fn()
return nil
}
func main() {
err := safeCall(func() {
panic("что-то пошло не так")
})
if err != nil {
fmt.Println("Перехвачено:", err)
}
}
Важно: panic/recover следует использовать только в критических сценариях (например, для восстановления после паники в горутинах или HTTP-обработчиках). В обычной логике всегда используйте возврат ошибок.
4. Best practices обработки ошибок в Go
Чтобы ваш код на Go был надежным и читаемым, следуйте этим рекомендациям:
- Всегда проверяйте ошибки. Игнорирование
errчерез_допустимо только если вы уверены в успехе (например,fmt.Fprint). - Не используйте панику как try/catch. Паника предназначена для ошибок, которые не должны происходить (выход за границы массива, nil pointer).
- Оборачивайте ошибки с контекстом. Вместо простого
return errиспользуйтеfmt.Errorf("что-то делали: %w", err). - Используйте
errors.Isиerrors.Asдля проверки цепочек ошибок, а не прямое сравнение. - Избегайте дублирования. Если одна и та же ошибка проверяется в нескольких местах, вынесите логику в функцию-