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.

Go error handling patterns and best practices in 2026

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.

Wartości błędów, nie wyjątki

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:

builtin.gogo
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:

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)
}

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.

errors.gogo
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.

Kiedy wartownicy stają się problemem

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:

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
}

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:

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 wyodrębnia konkretny typ błędu z łańcucha:

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
}

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.

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
}

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:

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)
}

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:

go
// 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:

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
}

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:

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 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 a obsługa błędów

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.Errorf i %w, by dodać kontekst zachowując łańcuch; %v stosuje się, by świadomie przerwać łańcuch na granicy abstrakcji
  • errors.Is należy preferować nad ==, a errors.As nad asercjami typów — obie przechodzą pełny opakowany łańcuch
  • Metoda Unwrap() error powinna 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ć errgroup zamiast 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

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

Udostępnij

Powiązane artykuły