Obsługa błędów w Go w 2026: wzorce, opakowywanie i pytania rekrutacyjne
Kompleksowy przewodnik po obsłudze błędów w Go: interfejs error, błędy wartownicze, opakowywanie z %w, errors.Is, errors.As oraz pytania pojawiające się na rozmowach kwalifikacyjnych.

Obsługa błędów w Go wyróżnia ten język na tle większości popularnych alternatyw. Zamiast wyjątków i bloków try/catch, Go traktuje błędy jako wartości — zwracane jawnie z funkcji i sprawdzane przy każdym wywołaniu. Ta przemyślana decyzja projektowa wymusza, by obsługa błędów znajdowała się na pierwszym planie, czyniąc ścieżki awarii widocznymi i łatwymi do testowania.
Go nie posiada mechanizmu wyjątków. Każda funkcja, która może się nie powieść, zwraca błąd jako ostatnią wartość zwracaną. Ten wzorzec sprawia, że koszt ignorowania błędów jest widoczny na poziomie kodu źródłowego — a przepływy błędów można trywialnie testować.
Interfejs error i dlaczego ma znaczenie
Cały system błędów w Go opiera się na jednym interfejsie zdefiniowanym w bibliotece standardowej:
type error interface {
Error() string
}Każdy typ implementujący metodę Error() string spełnia ten interfejs. Ta prostota napędza kompozycyjność: błędy mogą być strukturami, wrapperami lub czymkolwiek innym — pod warunkiem, że produkują reprezentację tekstową.
Podstawowy niestandardowy typ błędu demonstruje ten wzorzec:
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)
}Typ NotFoundError przenosi ustrukturyzowane dane. Wywołujący mogą wyodrębnić nazwę zasobu lub identyfikator programowo, zamiast parsować ciąg znaków — kluczowa przewaga przy budowaniu API HTTP lub narzędzi CLI, które mapują błędy na konkretne odpowiedzi.
Błędy wartownicze dla znanych warunków
Błędy wartownicze (sentinel errors) to zmienne na poziomie pakietu reprezentujące konkretne, dobrze znane warunki awarii. Biblioteka standardowa szeroko wykorzystuje ten wzorzec: io.EOF, sql.ErrNoRows, os.ErrNotExist.
var (
ErrNotFound = errors.New("record not found")
ErrUnauthorized = errors.New("unauthorized access")
ErrConflict = errors.New("resource conflict")
)Błędy wartownicze sprawdzają się najlepiej, gdy wywołujący musi jedynie wiedzieć, co się nie powiodło, a nie dlaczego w szczegółach. Sygnalizują warunek bez przenoszenia dodatkowego kontekstu. Konwencja Go przewiduje prefiks Err oraz treść komunikatu pisaną małymi literami, bez interpunkcji, zgodnie z wytycznymi Go Code Review Comments.
Eksportowanie błędów wartowniczych tworzy kontrakt publicznego API. Pakiety zależne będą je dopasowywać za pomocą errors.Is, więc zmiana nazwy lub usunięcie błędu wartowniczego to zmiana łamiąca kompatybilność. Należy ich używać oszczędnie — tylko dla warunków, na podstawie których wywołujący faktycznie muszą podejmować decyzje.
Opakowywanie błędów za pomocą fmt.Errorf i czasownika %w
Opakowywanie błędów, wprowadzone w Go 1.13, dodaje kontekst do błędu, zachowując oryginał w łańcuchu. Czasownik %w w fmt.Errorf tworzy ten łańcuch:
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
}Opakowany błąd zachowuje pełny łańcuch. Wywołujący trzy warstwy wyżej wciąż może dopasować oryginalnego wartownika lub wyodrębnić oryginalny typ. Każda warstwa dodaje kontekst o tym, gdzie wystąpił błąd, nie zaciemniając tego, co się stało.
Kluczowe rozróżnienie: %w opakowuje (zachowując łańcuch), podczas gdy %v formatuje błąd jako ciąg znaków i przerywa łańcuch. %v należy stosować świadomie, gdy oryginalny błąd nie powinien przenikać przez granicę abstrakcji — na przykład gdy repozytorium opakowuje błąd sterownika bazy danych, którego warstwa serwisowa nigdy nie powinna analizować.
Inspekcja błędów za pomocą errors.Is i errors.As
Pakiet errors udostępnia dwie funkcje do inspekcji opakowanych łańcuchów błędów, zastępując bezpośrednie porównanie i asercję typów.
errors.Is przechodzi łańcuch, szukając konkretnej wartości błędu:
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 wyodrębnia konkretny typ błędu z łańcucha:
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
}Kluczowa przewaga nad asercjami typów: obie funkcje przechodzą cały opakowany łańcuch. NotFoundError opakowany trzykrotnie za pomocą fmt.Errorf i %w wciąż zostanie dopasowany.
Ustrukturyzowana obsługa błędów w aplikacjach warstwowych
Produkcyjne aplikacje Go organizują błędy zazwyczaj w trzech warstwach: błędy domenowe definiują warunki na poziomie biznesowym, błędy serwisowe dodają kontekst operacyjny, a błędy handlerów lub warstwy transportowej mapują je na zewnętrzne odpowiedzi.
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
}Metoda Unwrap sprawia, że errors.Is i errors.As działają przez cały łańcuch. Każdy niestandardowy typ błędu opakowujący inny błąd powinien ją implementować.
Warstwa serwisowa opakowuje błędy domenowe kontekstem operacyjnym:
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)
}Gotowy na rozmowy o Go?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Popularne pytania rekrutacyjne dotyczące obsługi błędów w Go
Rozmowy kwalifikacyjne na stanowiska Go-developerów regularnie testują wiedzę z zakresu obsługi błędów. Poniższe pytania pojawiają się często w rundach selekcyjnych.
Dlaczego Go używa wartości błędów zamiast wyjątków?
Wyjątki tworzą niewidoczny przepływ sterowania. Funkcja, która rzuca wyjątek, przekazuje sterowanie do nieznanego miejsca catch, potencjalnie wiele ramek stosu dalej. Wartości błędów w Go czynią każdą ścieżkę awarii jawną w sygnaturze funkcji. Wywołujący natychmiast decyduje, jak obsłużyć błąd — ponowić próbę, opakować, zalogować lub propagować. Eliminuje to problem "niespodziewanego rzutu" i sprawia, że przepływy błędów są czytelne bez wsparcia narzędzi.
Jaka jest różnica między %w a %v w fmt.Errorf?
%w opakowuje błąd, zachowując łańcuch dla errors.Is i errors.As. %v formatuje błąd jako ciąg znaków, produkując nowy błąd bez powiązania z oryginałem. %w stosuje się, gdy wywołujący powinni móc analizować przyczynę; %v — gdy przyczyna jest szczegółem implementacji, który nie powinien przenikać przez granicę abstrakcji.
Kiedy funkcja powinna zwrócić error, a kiedy wywołać panic?
Panic jest zarezerwowany dla prawdziwie nieodwracalnych sytuacji: błędów programistycznych takich jak przekroczenie indeksu, dereferencja wskaźnika nil czy naruszone niezmienniki wskazujące na buga. Odwracalne awarie — limity czasu sieci, brakujące rekordy, nieprawidłowe dane wejściowe — zwracają błędy. Przydatna reguła: jeśli warunek może wystąpić podczas normalnej pracy, zwraca się error. Jeśli oznacza to, że program ma buga — wywołuje się panic.
Jak metoda Unwrap wpływa na inspekcję łańcucha błędów?
Gdy niestandardowy typ błędu implementuje Unwrap() error, funkcje errors.Is i errors.As podążają za tą metodą, by przejść łańcuch. Bez Unwrap łańcuch zatrzymuje się na tym błędzie. Od Go 1.20 błędy mogą również implementować Unwrap() []error, zwracając wiele opakowanych błędów, co umożliwia łańcuchy o strukturze drzewa — na przykład przy agregacji błędów walidacji.
Antywzorce obsługi błędów, których należy unikać
Kilka antywzorców pojawia się regularnie w kodzie Go i podczas sesji code review:
Ciche ignorowanie błędów. Pusty identyfikator sprawia, że jest to składniowo łatwe, ale semantycznie niebezpieczne:
// anti-pattern: silent error swallowing
result, _ := riskyOperation()Każdy zignorowany błąd powinien być świadomą, udokumentowaną decyzją — nigdy skrótem.
Logowanie i zwracanie jednocześnie. Ten wzorzec produkuje zduplikowane wpisy w logach i myli wywołującego, który sam również obsługuje błąd:
// 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
}Rozwiązanie: albo obsłużyć błąd (zalogować, zwrócić wartość domyślną, ponowić próbę), albo go propagować. Nie jedno i drugie.
Dopasowywanie ciągów znaków do klasyfikacji błędów. Sprawdzanie wyjścia err.Error() za pomocą strings.Contains jest kruche. Komunikaty błędów nie stanowią kontraktu API — mogą się zmieniać między wersjami bibliotek. Do wartości wartowniczych należy używać errors.Is, a do typów — errors.As.
Opakowywanie błędów przekraczających granice goroutine bez synchronizacji. Gdy wiele goroutines produkuje błędy jednocześnie, należy użyć errgroup.Group z pakietu golang.org/x/sync lub podobnego mechanizmu synchronizacji. Bezpośrednie dodawanie do współdzielonego slice bez muteksu tworzy wyścigi danych.
Obsługa błędów z errgroup dla operacji współbieżnych
Pakiet errgroup z golang.org/x/sync zapewnia czysty wzorzec zbierania błędów z współbieżnych goroutines:
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 anuluje pochodny kontekst, gdy dowolna goroutine zwróci błąd, sygnalizując pozostałym, by się zatrzymały. Zapobiega to marnowaniu pracy i dostarcza wywołującemu pojedynczy, opakowany błąd.
Go 1.24 (luty 2025) nie zmieniło pakietu errors. API obsługi błędów jest stabilne od Go 1.13, z dodatkiem wielokrotnego rozpakowywania błędów przez Unwrap() []error w Go 1.20. Obecne propozycje ulepszenia składni obsługi błędów pozostają w dyskusji w Go issue tracker.
Podsumowanie
- Błędy należy traktować jako wartości: zwracać je jawnie i obsługiwać przy każdym wywołaniu, zamiast polegać na ukrytych mechanizmach wyjątków
- Błędów wartowniczych (
var ErrX = errors.New(...)) należy używać wyłącznie dla warunków, na podstawie których wywołujący muszą podejmować decyzje — traktując je jak publiczne API - Błędy opakowuje się za pomocą
fmt.Errorfi%w, by dodać kontekst zachowując łańcuch;%vstosuje się, by świadomie przerwać łańcuch na granicy abstrakcji errors.Isnależy preferować nad==, aerrors.Asnad asercjami typów — obie przechodzą pełny opakowany łańcuch- Metoda
Unwrap() errorpowinna być implementowana w niestandardowych typach błędów, by inspekcja łańcucha działała poprawnie - Błąd obsługuje się lub propaguje — nigdy oba naraz. Logowanie błędu i jednoczesne jego zwracanie produkuje zduplikowany szum
- Do współbieżnego zbierania błędów należy używać
errgroupzamiast ręcznej synchronizacji goroutines - Komunikaty błędów powinny być pisane małymi literami, bez interpunkcji, zgodnie z konwencjami Go
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Tagi
Udostępnij
Powiązane artykuły

Wzorce projektowe w Go: kluczowe wzorce i pytania rekrutacyjne dla programistów Go
Przegląd kluczowych wzorców projektowych w Go: Functional Options, Strategy, Factory, Observer oraz Middleware. Praktyczne przykłady kodu i pytania rekrutacyjne dla programistów Go.

Go 1.26 na rozmowie rekrutacyjnej: Green Tea GC, go fix i optymalizacje stosu
Kompletny przewodnik po kluczowych zmianach w Go 1.26 pod katem pytan rekrutacyjnych. Garbage collector Green Tea, narzedzie go fix z modernizatorami, alokacja slice'ow na stosie, nowa skladnia new(), typy generyczne z samoodniesieniem i wykrywanie wyciekow goroutine.

25 najczęstszych pytań rekrutacyjnych Go: kompletny przewodnik dewelopera
Opanuj rozmowy o pracę z Go, znając 25 najczęstszych pytań. Goroutiny, kanały, interfejsy i wzorce współbieżności z przykładami kodu.