Menu

Obsługa błędów w Go: if err != nil, opakowywanie i sprawdzanie

Go obsługuje błędy jako zwykłe wartości zwracane z funkcji. Poznaj interfejs error, if err != nil, errors.New i fmt.Errorf, zwracanie błędów z kontekstem, sprawdzanie ich przez errors.Is i errors.As oraz obsługę każdego błędu tylko raz.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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ć, gdy err != nil.
  • if err := f(); err != nil ogranicza zasięg err do if, 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) przez errors.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.As przyjmuje 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, po Flush.
  • 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żyj errors.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 e dla wywołującego nie jest nil. Zwracaj dosłowne nil.
  • Komunikaty z wielkiej litery lub z interpunkcją na końcu. errors.New("Failed to connect.") po opakowaniu źle się czyta. Pisz connect 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().

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ