throw new Error() / try-catch → возврат ошибки как значения
error.code === 'ENOENT' → errors.Is(err, ErrNotFound)
error instanceof ValidationError → errors.As(err, &valErr)
error.cause → fmt.Errorf("...: %w", err) — оборачивание
Нет Promise.reject → все ошибки синхронные (для асинхронщины — каналы)
error — самый простой в Go
var ErrX = errors.New(...))errorfmt.Errorf("...: %w", err)errors.Is() и errors.As()mkdir go-errors && cd go-errors
go mod init go-errors
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"os"
"time"
)
// ╔══════════════════════════════════════════════════════════╗
// ║ 1. SENTINEL ERRORS (сторожевые ошибки) ║
// ╚══════════════════════════════════════════════════════════╝
// Sentinel errors — предопределённые ошибки уровня пакета.
// Их можно сравнивать через errors.Is().
// По соглашению: ErrXxx или errXxx (если не экспортируются).
var (
ErrNotFound = errors.New("resource not found")
ErrValidation = errors.New("validation failed")
ErrTimeout = errors.New("operation timed out")
ErrUnauthorized = errors.New("unauthorized")
ErrInternalServer = errors.New("internal server error")
)
// ╔══════════════════════════════════════════════════════════╗
// ║ 2. КАСТОМНЫЕ ТИПЫ ОШИБОК ║
// ╚══════════════════════════════════════════════════════════╝
// ValidationError — ошибка валидации с контекстом.
// Реализует интерфейс error (метод Error() string).
type ValidationError struct {
Field string // Какое поле не прошло валидацию
Value any // Какое значение получено
Message string // Человекочитаемое сообщение
}
// Error() — реализация интерфейса error.
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on field %q (got %v): %s",
e.Field, e.Value, e.Message)
}
// Дополнительный метод для извлечения поля (через errors.As)
func (e *ValidationError) FieldName() string {
return e.Field
}
// QueryError — ошибка с кодом (пример для БД/API)
type QueryError struct {
Query string
Code int
Cause error // исходная ошибка
}
func (e *QueryError) Error() string {
return fmt.Sprintf("query %q failed with code %d: %v",
e.Query, e.Code, e.Cause)
}
// Unwrap — метод для цепочки ошибок. Без него errors.Is/As не видят Cause.
func (e *QueryError) Unwrap() error {
return e.Cause
}
// TemporaryError — временная ошибка (можно повторить)
type TemporaryError struct {
Operation string
RetryAfter time.Duration
}
func (e *TemporaryError) Error() string {
return fmt.Sprintf("temporary error in %s, retry after %s",
e.Operation, e.RetryAfter)
}
// Temporary() bool — кастомный метод для проверки (можно использовать с errors.As)
func (e *TemporaryError) Temporary() bool {
return true
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 3. СЕРВИСНЫЙ СЛОЙ (демонстрация оборачивания) ║
// ╚══════════════════════════════════════════════════════════╝
// Repository — имитация слоя данных
type Repository struct{}
func (r *Repository) FindUser(ctx context.Context, id string) (string, error) {
switch id {
case "":
return "", &ValidationError{
Field: "id", Value: id, Message: "must not be empty",
}
case "404":
// Возвращаем sentinel error
return "", ErrNotFound
case "timeout":
// Оборачиваем sentinel error с дополнительным контекстом
return "", fmt.Errorf("FindUser timeout for id=%s: %w", id, ErrTimeout)
case "db-error":
// Имитация ошибки БД
return "", &QueryError{
Query: "SELECT * FROM users WHERE id = $1",
Code: 500,
Cause: sql.ErrNoRows, // оборачиваем реальную ошибку
}
case "temp":
return "", &TemporaryError{
Operation: "FindUser", RetryAfter: 5 * time.Second,
}
default:
return "user_" + id, nil
}
}
// Service — бизнес-логика (оборачивает ошибки репозитория)
type Service struct {
repo *Repository
}
func (s *Service) GetUser(ctx context.Context, id string) (string, error) {
user, err := s.repo.FindUser(ctx, id)
if err != nil {
// Добавляем контекст к ошибке, НЕ теряя исходную (через %w)
return "", fmt.Errorf("GetUser(id=%s): %w", id, err)
}
return user, nil
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 4. ОБРАБОТЧИК ОШИБОК (error handler) ║
// ╚══════════════════════════════════════════════════════════╝
// HandleError — демонстрирует ВСЕ способы проверки ошибок
func HandleError(err error) {
if err == nil {
fmt.Println("✅ Успех!")
return
}
fmt.Printf("❌ Ошибка: %v\n", err)
fmt.Printf(" Подробно: %+v\n", err)
// === 4.1 errors.Is — проверка sentinel errors и цепочки ===
// Проходит по всей цепочке Unwrap(), ищет target.
if errors.Is(err, ErrNotFound) {
fmt.Println(" → Ресурс не найден (sentinel)")
}
if errors.Is(err, ErrTimeout) {
fmt.Println(" → Таймаут (sentinel, даже через обёртку)")
}
if errors.Is(err, ErrValidation) {
fmt.Println(" → Ошибка валидации (sentinel)")
}
if errors.Is(err, sql.ErrNoRows) {
fmt.Println(" → sql.ErrNoRows (даже через QueryError!)")
}
// === 4.2 errors.As — извлечение кастомного типа ===
// Извлекает ПЕРВЫЙ подходящий тип из цепочки.
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf(" → ValidationError: поле=%q значение=%v\n",
valErr.FieldName(), valErr.Value)
}
var queryErr *QueryError
if errors.As(err, &queryErr) {
fmt.Printf(" → QueryError: запрос=%q код=%d\n",
queryErr.Query, queryErr.Code)
}
var tempErr *TemporaryError
if errors.As(err, &tempErr) {
fmt.Printf(" → TemporaryError: операция=%s повтор через=%s\n",
tempErr.Operation, tempErr.RetryAfter)
}
fmt.Println()
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 5. ERRORS.JOIN — объединение ошибок (Go 1.20+) ║
// ╚══════════════════════════════════════════════════════════╝
func validateMultiple(fields map[string]string) error {
var errs []error // слайс ошибок
for field, value := range fields {
if value == "" {
errs = append(errs, &ValidationError{
Field: field, Value: value, Message: "must not be empty",
})
}
}
if len(errs) == 0 {
return nil
}
// errors.Join объединяет несколько ошибок в одну.
// errors.Is проверяет КАЖДУЮ.
return errors.Join(errs...)
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 6. DEFER + RECOVER — ловля паник ║
// ╚══════════════════════════════════════════════════════════╝
// ВАЖНО: Паника ≠ Ошибка. Паника — это критический сбой (как throw в JS).
// Не используй panic/recover для обычной обработки ошибок!
func riskyOperation(willPanic bool) (err error) {
// defer + recover — аналог try-catch, но ТОЛЬКО для паник
defer func() {
if r := recover(); r != nil {
// Преобразуем панику в ошибку
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
if willPanic {
panic("что-то пошло не так!") // аналог throw
}
return nil
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 7. BEST PRACTICES: ОБОРАЧИВАНИЕ ОШИБОК ║
// ╚══════════════════════════════════════════════════════════╝
// WrapWithContext — паттерн: добавляем контекст, сохраняя исходную ошибку
func WrapWithContext(op string, err error) error {
return fmt.Errorf("%s: %w", op, err)
}
// IsTemporary — хелпер для проверки временных ошибок
func IsTemporary(err error) bool {
var tempErr *TemporaryError
return errors.As(err, &tempErr)
}
// ╔══════════════════════════════════════════════════════════╗
// ║ MAIN ║
// ╚══════════════════════════════════════════════════════════╝
func main() {
fmt.Println("╔══════════════════════════════════════════╗")
fmt.Println("║ ОБРАБОТКА ОШИБОК — ПРОДВИНУТЫЕ ПАТТЕРНЫ ║")
fmt.Println("╚══════════════════════════════════════════╝")
repo := &Repository{}
svc := &Service{repo: repo}
ctx := context.Background()
// ==========================================
// 1. Тестируем разные сценарии ошибок
// ==========================================
fmt.Println("── 1. СЦЕНАРИИ ОШИБОК ──\n")
testCases := []struct {
name string
id string
}{
{"Успешный запрос", "123"},
{"Пустой ID (ValidationError)", ""},
{"Не найден (sentinel)", "404"},
{"Таймаут (wrapped sentinel)", "timeout"},
{"Ошибка БД (QueryError)", "db-error"},
{"Временная ошибка (TemporaryError)", "temp"},
}
for _, tc := range testCases {
fmt.Printf(">>> %s (id=%q)\n", tc.name, tc.id)
user, err := svc.GetUser(ctx, tc.id)
if err == nil {
fmt.Printf(" Пользователь: %s\n\n", user)
} else {
HandleError(err)
}
}
// ==========================================
// 2. errors.Join
// ==========================================
fmt.Println("── 2. ERRORS.JOIN ──")
fields := map[string]string{
"name": "",
"email": "",
"age": "25",
}
err := validateMultiple(fields)
if err != nil {
fmt.Printf("Объединённая ошибка:\n%v\n", err)
// Проверка: содержит ли объединённая ошибка ValidationError?
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf(" Первая ValidationError: поле=%q\n", valErr.FieldName())
}
// Сколько всего ошибок?
if unwrapped := errors.Unwrap(err); unwrapped != nil {
fmt.Printf(" Unwrap первой: %v\n", unwrapped)
}
}
fmt.Println()
// ==========================================
// 3. Panic + Recover
// ==========================================
fmt.Println("── 3. PANIC + RECOVER ──")
err = riskyOperation(false)
fmt.Printf("Без паники: err=%v\n", err)
err = riskyOperation(true)
fmt.Printf("С паникой: err=%v\n", err)
// ==========================================
// 4. Паттерны оборачивания
// ==========================================
fmt.Println("\n── 4. ОБОРАЧИВАНИЕ ──")
// Создаём цепочку ошибок
baseErr := io.EOF
wrapped1 := fmt.Errorf("reading config: %w", baseErr)
wrapped2 := fmt.Errorf("initialization: %w", wrapped1)
wrapped3 := fmt.Errorf("server startup: %w", wrapped2)
fmt.Printf("Цепочка: %v\n", wrapped3)
fmt.Printf(" errors.Is(baseErr, io.EOF) = %v\n", errors.Is(wrapped3, io.EOF))
fmt.Printf(" errors.Is(baseErr, ErrNotFound) = %v\n", errors.Is(wrapped3, ErrNotFound))
// ==========================================
// 5. ПРОВЕРКА ВРЕМЕННОЙ ОШИБКИ
// ==========================================
fmt.Println("\n── 5. ВРЕМЕННЫЕ ОШИБКИ ──")
tempErr := &TemporaryError{Operation: "connect", RetryAfter: time.Second}
fmt.Printf("tempErr: %v\n", tempErr)
fmt.Printf("IsTemporary(tempErr) = %v\n", IsTemporary(tempErr))
fmt.Printf("IsTemporary(ErrNotFound) = %v\n", IsTemporary(ErrNotFound))
}
Handler (HTTP/gRPC) │ ├─ Ошибка? → логируем, маппим на статус-код │ Service (бизнес-логика) │ ├─ Ошибка из Repository → оборачиваем: fmt.Errorf(“GetUser: %w”, err) │ Repository (доступ к данным) │ ├─ Ошибка драйвера → оборачиваем: fmt.Errorf(“FindUser: %w”, err) │ Driver (pgx, mongo, etc.) │ └─ Исходная ошибка
КЛИЕНТ (HTTP-ответ) │ ├─ errors.Is(err, ErrNotFound) → 404 ├─ errors.Is(err, ErrValidation) → 400 ├─ errors.Is(err, ErrTimeout) → 504 ├─ errors.As(err, &valErr) → 422 с деталями └─ остальное → 500
| Метод | Назначение | Пример |
|---|---|---|
err == ErrXxx | Прямое сравнение (НЕ для обёрнутых!) | if err == ErrNotFound {} |
errors.Is(err, target) | Проверка по цепочке Unwrap() | errors.Is(err, io.EOF) |
errors.As(err, &target) | Извлечение типа из цепочки | errors.As(err, &valErr) |
errors.Unwrap(err) | Извлечение вложенной ошибки | cause := errors.Unwrap(err) |
errors.Join(errs...) | Объединение нескольких ошибок | errors.Join(err1, err2) |
# Запуск
go run main.go
# Сборка
go build -o errors-demo main.go
./errors-demo
if err == io.EOF не сработает, если ошибка обёрнута. Используй errors.Is().
return errors.New("своя ошибка") поверх err. Используй %w._ = doSomething() — красный флаг на code review.💡 Best practices от сеньоров:
error — интерфейс с одним методом Error() string. Возвращай, не бросай.
fmt.Errorf("контекст: %w", err) сохраняет цепочку ошибок.if err != nil — самый частый паттерн в Go.💡 Для Node.js разработчика:
errors.Is — аналог проверки error.code === 'ENOENT', но типобезопасно.errors.As — аналог if (error instanceof ValidationError).%w — как error.cause в JS.Promise.catch — для асинхронных ошибок используются каналы или errgroup (урок 10).