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

Обробка помилок у Go виділяє цю мову серед більшості популярних альтернатив. Замість механізму винятків і блоків try/catch, Go розглядає помилки як значення — вони явно повертаються з функцій і перевіряються на кожному місці виклику. Це продумане проєктне рішення виводить обробку помилок на перший план, роблячи шляхи збоїв видимими та придатними для тестування.
Go не має механізму винятків. Кожна функція, яка може завершитися невдачею, повертає помилку як останнє значення, що повертається. Цей патерн робить вартість ігнорування помилок видимою на рівні вихідного коду, а потоки помилок — тривіально тестованими.
Інтерфейс error і чому він важливий
Вся система помилок Go базується на єдиному інтерфейсі, визначеному в стандартній бібліотеці:
type error interface {
Error() string
}Будь-який тип, що реалізує Error() string, задовольняє цей інтерфейс. Ця простота забезпечує композиційність: помилки можуть бути структурами, обгортками або чим завгодно іншим — за умови, що вони створюють рядкове представлення.
Базовий користувацький тип помилки демонструє цей патерн:
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.
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 створює цей ланцюжок:
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 проходить ланцюжком у пошуку конкретного значення помилки:
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 витягує конкретний тип помилки з ланцюжка:
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, ¬Found) {
// 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 зазвичай організовують помилки у три шари: доменні помилки визначають умови бізнес-рівня, сервісні помилки додають операційний контекст, а помилки обробників або транспортного рівня відображають їх на зовнішні відповіді.
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 працювати через ланцюжок. Будь-який користувацький тип помилки, що обгортає іншу помилку, повинен його реалізовувати.
Сервісний рівень обгортає доменні помилки операційним контекстом:
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 та під час сесій код-рев'ю:
Мовчазне ігнорування помилок. Порожній ідентифікатор робить це синтаксично простим, але семантично небезпечним:
// anti-pattern: silent error swallowing
result, _ := riskyOperation()Кожна проігнорована помилка повинна бути свідомим, задокументованим рішенням — ніколи скороченням.
Логування і повернення одночасно. Цей патерн створює дубльовані записи в логах і плутає викликаючого, який також обробляє помилку:
// 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 надає чистий патерн для збирання помилок з конкурентних горутин:
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 (лютий 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: ключові патерни та питання для співбесід Go-розробників
Огляд ключових патернів проєктування в Go: Functional Options, Strategy, Factory, Observer та Middleware. Практичні приклади коду та питання для технічних співбесід Go-розробників.

Go 1.26 на співбесіді: Green Tea GC, go fix та оптимізація стеку
Ключові питання та відповіді з Go 1.26 для технічних співбесід: збирач сміття Green Tea зі зниженням навантаження на 10-40%, оновлений go fix з модернізаторами, алокація slice на стеку, виявлення витоків горутин та постквантова криптографія.

Топ 25 запитань на співбесіду з Go: повний посібник для розробника
Підкорюйте співбесіди з Go завдяки 25 найпоширенішим запитанням. Горутини, канали, інтерфейси та патерни конкурентності з прикладами коду.