Błędy są wartościami
Funkcja w Go, która może się nie powieść, zwraca error jako ostatni wynik. Wywołujący od razu go sprawdza.
Wynik:
parsed: 42
could not parse: strconv.Atoi: parsing "forty-two": invalid syntax
To cały mechanizm. Nie ma wyjątków, nie ma try ani catch i nie ma ukrytego przepływu sterowania: błąd trafia tylko tam, dokąd przekaże go twój kod. Ceną jest widoczne powtarzanie if err != nil. Korzyścią jest to, że każdy punkt, w którym coś może pójść źle, widać w kodzie, a w każdym z nich decydujesz, co się stanie.
Typ error
error to wbudowany interfejs z jedną metodą:
type error interface {
Error() string
}
Każdy typ z metodą Error() string jest błędem. Błąd nil oznacza sukces. Wypisanie błędu przez fmt.Println(err) lub %v wywołuje Error().
Tworzenie błędów
Dwie funkcje pokrywają większość przypadków.
errors.New tworzy błąd ze stałym tekstem. fmt.Errorf go formatuje, z tymi samymi czasownikami co Printf. Zgodnie z konwencją komunikaty błędów zaczynają się małą literą i nie mają kropki na końcu, bo zwykle są wstawiane w dłuższe komunikaty: load config: open app.yaml: no such file or directory.
Wzorzec if err != nil
Idiomatyczny schemat to: wywołaj, sprawdź, wyjdź wcześnie. Ścieżka sukcesu zostaje przy lewym marginesie, a każde niepowodzenie kończy funkcję od razu, gdy wystąpi.
func loadUser(id int) (*User, error) {
row, err := db.Query(id)
if err != nil {
return nil, err
}
u, err := parseUser(row)
if err != nil {
return nil, err
}
if err := u.Validate(); err != nil {
return nil, err
}
return u, nil
}
Dwie konwencje, na które warto zwrócić uwagę:
- Przy błędzie zwracaj wartość zerową dla pozostałych wyników (
nil,0,""). Wywołujący nie mogą ich używać, gdyerr != nil. if err := f(); err != nilogranicza zasięgerrdoif, gdy funkcja zwraca tylko błąd. Dzięki temu zewnętrzny zakres pozostaje czysty.
Unikaj else po zwróceniu błędu. if err != nil { return err } else { ... } tylko bez powodu wcina ścieżkę sukcesu.
Dodawanie kontekstu przy zwracaniu błędu
Błąd przekazany wyżej bez zmian traci historię tego, skąd pochodzi. open config.yaml: no such file or directory nie mówi, który krok uruchamiania się nie powiódł. Dodaj kontekst przez fmt.Errorf i czasownik %w:
Wynik:
start server: read config: open /etc/myapp/config.yaml: no such file or directory
true
Każda warstwa dopisuje, co robiła, a końcowy komunikat czyta się jak ślad od góry wywołania aż do przyczyny. Dobry kontekst nazywa operację i dane wejściowe: parse line 12, fetch user 42. Nie dopisuj "error" ani "failed" na każdym poziomie; komunikat i tak jest błędem.
%w opakowuje: trzyma oryginalny błąd wewnątrz nowego, więc errors.Is i errors.As nadal mogą go znaleźć. %v kopiuje tylko tekst. Używaj %v, gdy celowo chcesz ukryć szczegół implementacji przed wywołującymi, na przykład żeby nie uzależnili się od typu błędu sterownika bazy danych.
Sprawdzanie konkretnych błędów: errors.Is i errors.As
Czasem wywołujący musi zareagować na jeden rodzaj niepowodzenia: brak pliku oznacza "użyj wartości domyślnych", timeout oznacza "spróbuj ponownie". Odpowiadają na to dwie funkcje i obie przeglądają każdą warstwę opakowania.
Praktyczne zasady:
- Porównuj z predefiniowanymi wartościami błędów (sentinelami takimi jak
io.EOF,os.ErrNotExist,sql.ErrNoRows) przezerrors.Is, a nie==.==przestaje działać, gdy błąd zostanie opakowany. - Typowany błąd wyciągaj przez
errors.As, a nie przez asercję typu, z tego samego powodu.errors.Asprzyjmuje wskaźnik na zmienną typu docelowego. - Nigdy nie dopasowuj po tekście
err.Error(). Komunikaty zmieniają się między wersjami, a dopasowanie tekstu psuje się wtedy po cichu.
Definiowanie własnych błędów sentinel i typów błędów oraz łączenie kilku błędów przez errors.Join opisuje strona o własnych błędach.
Obsługuj błąd raz
Błąd powinien zostać obsłużony dokładnie raz. Obsługa oznacza jedno z: zwrócenie go (zwykle opakowanego), zalogowanie i kontynuowanie, ponowienie próby albo zamianę na odpowiedź dla użytkownika. Robienie dwóch z tych rzeczy naraz to najczęstszy błąd związany z błędami w kodzie Go.
// Wrong: logged here, and returned, so it is logged again by every caller.
if err != nil {
log.Printf("could not fetch user: %v", err)
return err
}
// Right: add context and return. The top of the program logs once.
if err != nil {
return fmt.Errorf("fetch user %d: %w", id, err)
}
Logowanie i zwracanie jednocześnie daje w logach tę samą awarię kilka razy, za każdym razem z mniejszym kontekstem niż końcowy komunikat. Pozwól błędom płynąć w górę do miejsca, które może zdecydować, co zrobić (handler HTTP, main, pętla workera), i tam je loguj.
Gdzie trafiają błędy
Na szczycie programu coś musi zareagować na błąd. W main zwykle oznacza to wypisanie go i wyjście z niezerowym statusem:
Uruchomienie bez argumentów wypisuje error: usage: app <name> na stderr i kończy program ze statusem 1 (wpisz imię w panelu Args, żeby zobaczyć drugą ścieżkę). Trzymanie main w tym kształcie, z właściwą pracą w run, sprawia, że program da się testować, a instrukcje defer wewnątrz run nadal się wykonują, bo os.Exit pomija odroczone wywołania.
W serwerze HTTP szczytem jest handler: mapuje błąd na kod statusu i bezpieczny komunikat dla klienta, a szczegółowy komunikat loguje dla ciebie.
Błędy, które można zignorować, i te, których nie wolno
Zignorowanie błędu bywa poprawne, ale zrób to jawnie przez _, żeby czytelnicy wiedzieli, że to była decyzja:
_ = conn.SetDeadline(t) // best effort
Niektóre wywołania w praktyce nie mogą się nie powieść (strings.Builder.WriteString, bytes.Buffer.Write). Inne wyglądają niewinnie, a takie nie są: Close na pliku, do którego zapisywano dane, może zgłosić, że dane nigdy nie trafiły na dysk, a json.Marshal zawodzi na kanałach i funkcjach. W razie wątpliwości sprawdzaj.
Linter errcheck (dołączony do golangci-lint) zgłasza niesprawdzone błędy. Samo go vet ich nie wyłapuje.
Błędy a panic
Go ma też panic, ale to nie jest system wyjątków. Używaj błędów do wszystkiego, co może pójść źle podczas normalnej pracy: złych danych wejściowych, brakujących plików, awarii sieci. Panic zostaw dla błędów w kodzie (niemożliwy stan, złamany niezmiennik) i dla awarii przy starcie, gdy dalsze działanie nie ma sensu. Biblioteka prawie nigdy nie powinna wywoływać panic przez swoje API. Zobacz panic i recover.
Mniej powtórzeń
if err != nil jest rozwlekłe, a propozycje dodania nowej składni do tego celu wielokrotnie odrzucano; w 2025 roku zespół Go ogłosił, że nie zajmuje się już zmianami składni obsługi błędów. Kilka wzorców zmniejsza szum w ramach samego języka:
- Wychodź wcześnie i pisz małe funkcje. Większość powtórzeń bierze się z długich funkcji wykonujących wiele kroków.
- Lepki błąd. Przy serii zapisów zachowaj pierwszy błąd w polu struktury, a gdy jest ustawiony, kolejne wywołania niech nic nie robią. Tak działa
bufio.Writer: sprawdzasz błąd raz, poFlush. - Opakowuj raz na funkcję. Odroczone domknięcie nad nazwanym wynikiem może dodać ten sam kontekst do każdego błędu zwracanego przez funkcję (zobacz defer).
Typowe błędy
- Używanie wartości, gdy err nie jest nil. Najpierw sprawdź, potem używaj.
- Logowanie i zwracanie jednocześnie. Wybierz jedno.
- Porównywanie przez
==po opakowaniu. Użyjerrors.Is. - Utrata przyczyny przez
%v. Użyj%w, chyba że chodzi właśnie o jej ukrycie. - Zwracanie typowanego wskaźnika nil jako
error.var e *MyErr; return edla wywołującego nie jest nil. Zwracaj dosłownenil. - Komunikaty z wielkiej litery lub z interpunkcją na końcu.
errors.New("Failed to connect.")po opakowaniu źle się czyta. Piszconnect to db: ....
Najczęściej zadawane pytania
Jak działa obsługa błędów w Go?
Funkcje, które mogą się nie powieść, zwracają error jako ostatni wynik. Wywołujący od razu go sprawdza: v, err := f(); if err != nil { return err }. error to zwykła wartość interfejsu z jedną metodą, Error() string, a nil oznacza sukces. Nie ma wyjątków.
Czy Go ma try/catch?
Nie. Go nie ma wyjątków ani try/catch. Spodziewane błędy są zwracane jako wartości error i sprawdzane przez if err != nil. panic i recover istnieją, ale służą do błędów programistycznych i stanów bez wyjścia, a nie do zwykłego przepływu błędów.
Jak zwrócić błąd w Go?
Zadeklaruj error jako ostatni wynik i przy sukcesie zwracaj nil. Błędy twórz przez errors.New("message") dla stałego tekstu albo fmt.Errorf("reading %s: %w", name, err), żeby dodać kontekst do otrzymanego błędu. Przy niepowodzeniu zwracaj wartości zerowe dla pozostałych wyników.
Czym różni się %w od %v w fmt.Errorf?
Oba wstawiają komunikat oryginalnego błędu do nowego. %w dodatkowo go opakowuje, więc errors.Is i errors.As nadal mogą znaleźć oryginał. %v tworzy nowy błąd zawierający tylko tekst. Używaj %w, gdy wywołujący mogą potrzebować sprawdzić przyczynę, a %v, gdy chcesz ją ukryć.
Jak sprawdzić, jaki błąd został zwrócony w Go?
Użyj errors.Is(err, target), żeby porównać z błędem sentinel, takim jak io.EOF czy os.ErrNotExist, oraz errors.As(err, &target), żeby wyciągnąć konkretny typ błędu, na przykład *fs.PathError. Obie funkcje przechodzą przez opakowane błędy. Unikaj porównywania stringów z err.Error().