Обробка помилок у Go у 2026 році: патерни, обгортання та питання технічних співбесід

Вичерпний посібник з обробки помилок у Go: інтерфейс error, сигнальні помилки, обгортання з %w, errors.Is, errors.As та питання, що зустрічаються на технічних співбесідах.

Go error handling patterns and best practices in 2026

Обробка помилок у Go виділяє цю мову серед більшості популярних альтернатив. Замість механізму винятків і блоків try/catch, Go розглядає помилки як значення — вони явно повертаються з функцій і перевіряються на кожному місці виклику. Це продумане проєктне рішення виводить обробку помилок на перший план, роблячи шляхи збоїв видимими та придатними для тестування.

Значення помилок, а не винятки

Go не має механізму винятків. Кожна функція, яка може завершитися невдачею, повертає помилку як останнє значення, що повертається. Цей патерн робить вартість ігнорування помилок видимою на рівні вихідного коду, а потоки помилок — тривіально тестованими.

Інтерфейс error і чому він важливий

Вся система помилок Go базується на єдиному інтерфейсі, визначеному в стандартній бібліотеці:

builtin.gogo
type error interface {
    Error() string
}

Будь-який тип, що реалізує Error() string, задовольняє цей інтерфейс. Ця простота забезпечує композиційність: помилки можуть бути структурами, обгортками або чим завгодно іншим — за умови, що вони створюють рядкове представлення.

Базовий користувацький тип помилки демонструє цей патерн:

apperror.gogo
type NotFoundError struct {
    Resource string
    ID       string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s with ID %s not found", e.Resource, e.ID)
}

Тип NotFoundError несе структуровані дані. Ті, хто викликає функцію, можуть програмно витягнути назву ресурсу або ідентифікатор замість парсингу рядка — критична перевага при побудові HTTP API або CLI-інструментів, де помилки маються відображатися на конкретні відповіді.

Сигнальні помилки для відомих умов

Сигнальні помилки (sentinel errors) — це змінні рівня пакета, що представляють конкретні, добре відомі умови збою. Стандартна бібліотека широко використовує цей патерн: io.EOF, sql.ErrNoRows, os.ErrNotExist.

errors.gogo
var (
    ErrNotFound     = errors.New("record not found")
    ErrUnauthorized = errors.New("unauthorized access")
    ErrConflict     = errors.New("resource conflict")
)

Сигнальні помилки найкраще підходять, коли викликаючій стороні потрібно знати лише що саме не вдалося, а не чому в деталях. Вони сигналізують про умову без додаткового контексту. Конвенція Go передбачає префікс Err та текст повідомлення малими літерами без пунктуації, відповідно до рекомендацій Go Code Review Comments.

Коли сигнальні помилки стають проблемою

Експортування сигнальних помилок створює контракт публічного API. Залежні пакети будуть зіставляти їх через errors.Is, тому перейменування або видалення сигнальної помилки є зламною зміною. Використовувати їх слід обережно — лише для умов, за якими викликаючі дійсно повинні здійснювати розгалуження.

Обгортання помилок за допомогою fmt.Errorf та дієслова %w

Обгортання помилок, запроваджене в Go 1.13, додає контекст до помилки, зберігаючи оригінал у ланцюжку. Дієслово %w у fmt.Errorf створює цей ланцюжок:

repository.gogo
func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) {
    user, err := r.db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", id)
    if err != nil {
        return nil, fmt.Errorf("UserRepo.FindByID(%s): %w", id, err)
    }
    return user, nil
}

Обгорнута помилка зберігає повний ланцюжок. Викликаючий на три рівні вище все ще може зіставити оригінальний сигнальний маркер або витягнути оригінальний тип. Кожен рівень додає контекст про те, де сталася помилка, не приховуючи що саме сталося.

Критичне розрізнення: %w обгортає (зберігаючи ланцюжок), тоді як %v форматує помилку як рядок і розриває ланцюжок. %v слід використовувати свідомо, коли оригінальна помилка не повинна просочуватися через межу абстракції — наприклад, коли репозиторій обгортає помилку драйвера бази даних, яку сервісний рівень ніколи не повинен аналізувати.

Інспекція помилок за допомогою errors.Is та errors.As

Пакет errors надає дві функції для інспекції обгорнутих ланцюжків помилок, що замінюють пряме порівняння та приведення типів.

errors.Is проходить ланцюжком у пошуку конкретного значення помилки:

handler.gogo
func handleGetUser(w http.ResponseWriter, r *http.Request) {
    user, err := userService.GetByID(r.Context(), chi.URLParam(r, "id"))
    if errors.Is(err, ErrNotFound) {
        // Matches even if ErrNotFound was wrapped multiple times
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }
    if err != nil {
        http.Error(w, "Internal error", http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(user)
}

errors.As витягує конкретний тип помилки з ланцюжка:

middleware.gogo
func errorMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if r := recover(); r != nil {
                http.Error(w, "Internal error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

func mapErrorToHTTP(err error) int {
    var notFound *NotFoundError
    if errors.As(err, &notFound) {
        // notFound.Resource and notFound.ID are available
        return http.StatusNotFound
    }
    var validationErr *ValidationError
    if errors.As(err, &validationErr) {
        return http.StatusBadRequest
    }
    return http.StatusInternalServerError
}

Ключова перевага над приведенням типів: обидві функції обходять увесь обгорнутий ланцюжок. NotFoundError, обгорнутий тричі за допомогою fmt.Errorf та %w, все одно буде зіставлений.

Структурована обробка помилок у багатошарових додатках

Продакшн-додатки на Go зазвичай організовують помилки у три шари: доменні помилки визначають умови бізнес-рівня, сервісні помилки додають операційний контекст, а помилки обробників або транспортного рівня відображають їх на зовнішні відповіді.

domain/errors.gogo
type DomainError struct {
    Code    string // machine-readable: "USER_NOT_FOUND", "INVALID_INPUT"
    Message string // human-readable description
    Err     error  // original cause
}

func (e *DomainError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("%s: %s: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("%s: %s", e.Code, e.Message)
}

func (e *DomainError) Unwrap() error {
    return e.Err
}

Метод Unwrap — саме те, що дозволяє errors.Is та errors.As працювати через ланцюжок. Будь-який користувацький тип помилки, що обгортає іншу помилку, повинен його реалізовувати.

Сервісний рівень обгортає доменні помилки операційним контекстом:

service/user.gogo
func (s *UserService) Deactivate(ctx context.Context, userID string) error {
    user, err := s.repo.FindByID(ctx, userID)
    if err != nil {
        return fmt.Errorf("deactivating user %s: %w", userID, err)
    }
    if user.Status == StatusInactive {
        return &DomainError{
            Code:    "ALREADY_INACTIVE",
            Message: fmt.Sprintf("user %s is already inactive", userID),
        }
    }
    return s.repo.UpdateStatus(ctx, userID, StatusInactive)
}

Готовий до співбесід з Go?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Поширені питання технічних співбесід з обробки помилок у Go

Технічні співбесіди на Go-позиції регулярно перевіряють знання обробки помилок. Наведені нижче питання часто зустрічаються під час відбіркових раундів.

Чому Go використовує значення помилок замість винятків?

Винятки створюють невидимий потік керування. Функція, що кидає виняток, передає керування до невідомого місця перехоплення, потенційно на багато фреймів стеку далі. Значення помилок у Go роблять кожен шлях збою явним у сигнатурі функції. Викликаючий одразу вирішує, як обробити помилку — повторити спробу, обгорнути, залогувати або пропагувати. Це усуває проблему "несподіваного кидка" і робить потоки помилок читабельними без інструментальної підтримки.

Яка різниця між %w та %v у fmt.Errorf?

%w обгортає помилку, зберігаючи ланцюжок для errors.Is та errors.As. %v форматує помилку як рядок, створюючи нову помилку без зв'язку з оригіналом. %w використовується, коли викликаючі повинні мати можливість аналізувати причину; %v — коли причина є деталлю реалізації, яка не повинна просочуватися через межу абстракції.

Коли функція повинна повертати error, а коли викликати panic?

Panic зарезервований для справді невідновлюваних ситуацій: помилок програмування, таких як вихід за межі індексу, розіменування нульового вказівника або порушені інваріанти, що вказують на баг. Відновлювані збої — мережеві тайм-аути, відсутні записи, невалідні вхідні дані — повертають помилки. Корисне правило: якщо умова може виникнути під час нормальної роботи, повертається error. Якщо це означає, що у програмі баг, — викликається panic.

Як метод Unwrap впливає на інспекцію ланцюжка помилок?

Коли користувацький тип помилки реалізує Unwrap() error, функції errors.Is та errors.As слідують за цим методом для обходу ланцюжка. Без Unwrap ланцюжок зупиняється на цій помилці. Починаючи з Go 1.20, помилки також можуть реалізовувати Unwrap() []error, повертаючи декілька обгорнутих помилок, що забезпечує деревоподібні ланцюжки помилок — наприклад, при агрегації помилок валідації.

Антипатерни обробки помилок, яких слід уникати

Декілька антипатернів регулярно з'являються у кодових базах Go та під час сесій код-рев'ю:

Мовчазне ігнорування помилок. Порожній ідентифікатор робить це синтаксично простим, але семантично небезпечним:

go
// anti-pattern: silent error swallowing
result, _ := riskyOperation()

Кожна проігнорована помилка повинна бути свідомим, задокументованим рішенням — ніколи скороченням.

Логування і повернення одночасно. Цей патерн створює дубльовані записи в логах і плутає викликаючого, який також обробляє помилку:

go
// anti-pattern: double handling
if err != nil {
    log.Printf("operation failed: %v", err) // logged here
    return err                               // AND returned to caller who may log again
}

Виправлення: або обробити помилку (залогувати, повернути значення за замовчуванням, повторити спробу), або пропагувати її. Не обидва варіанти одночасно.

Зіставлення рядків для класифікації помилок. Перевірка виводу err.Error() за допомогою strings.Contains є крихкою. Повідомлення помилок не є контрактом API — вони можуть змінюватися між версіями бібліотек. Для сигнальних значень слід використовувати errors.Is, а для типів — errors.As.

Обгортання помилок, що перетинають межі горутин, без синхронізації. Коли декілька горутин генерують помилки одночасно, необхідно використовувати errgroup.Group з пакета golang.org/x/sync або подібний механізм синхронізації. Безпосереднє додавання до спільного slice без мʼютекса створює гонки даних.

Обробка помилок з errgroup для конкурентних операцій

Пакет errgroup з golang.org/x/sync надає чистий патерн для збирання помилок з конкурентних горутин:

batch.gogo
func (s *OrderService) ProcessBatch(ctx context.Context, orderIDs []string) error {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(10) // limit concurrent goroutines

    for _, id := range orderIDs {
        g.Go(func() error {
            if err := s.processOrder(ctx, id); err != nil {
                return fmt.Errorf("processing order %s: %w", id, err)
            }
            return nil
        })
    }

    // Returns the first non-nil error and cancels remaining work
    return g.Wait()
}

errgroup.WithContext скасовує похідний контекст, коли будь-яка горутина повертає помилку, сигналізуючи іншим зупинитися. Це запобігає марній роботі та надає викликаючому єдину обгорнуту помилку.

Go 1.24 та обробка помилок

Go 1.24 (лютий 2025) не вніс змін до пакета errors. API обробки помилок залишається стабільним з Go 1.13, з додаванням множинного розгортання помилок через Unwrap() []error у Go 1.20. Поточні пропозиції щодо покращеного синтаксису обробки помилок залишаються на обговоренні в Go issue tracker.

Висновок

  • Помилки слід розглядати як значення: повертати їх явно й обробляти на кожному місці виклику, замість покладатися на приховані механізми винятків
  • Сигнальні помилки (var ErrX = errors.New(...)) слід використовувати лише для умов, за якими викликаючі повинні здійснювати розгалуження, і розглядати їх як публічне API
  • Помилки обгортаються за допомогою fmt.Errorf та %w для додавання контексту зі збереженням ланцюжка; %v застосовується для свідомого розриву ланцюжка на межі абстракції
  • errors.Is слід віддавати перевагу над ==, а errors.As — над приведенням типів, оскільки обидві функції обходять увесь обгорнутий ланцюжок
  • Метод Unwrap() error необхідно реалізовувати у користувацьких типах помилок, щоб інспекція ланцюжка працювала коректно
  • Помилку слід або обробляти, або пропагувати — ніколи обидва варіанти одночасно. Логування помилки з одночасним її поверненням створює дубльований шум
  • Для конкурентного збирання помилок слід використовувати errgroup замість ручної синхронізації горутин
  • Повідомлення помилок мають бути написані малими літерами, без пунктуації, відповідно до конвенцій Go

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Теги

#go
#error-handling
#best-practices
#interview

Поділитися

Пов'язані статті