Historia pytania:
Go wyróżnia się na tle wielu języków podejściem do obsługi błędów, w którym błędy są wartościami, a nie wyjątkami. Taki projekt został wybrany dla przejrzystości i prostoty: zawsze, gdy coś może pójść nie tak, funkcja jawnie zwraca błąd jako drugą wartość zwrotną.
Problem:
Wielu nowicjuszy stara się zastosować w Go znane wzorce try-catch, gubi się w dużej ilości sprawdzania błędów lub nie używa opakowywania błędów do przekazywania kontekstu. Prowadzi to do utraty informacji i trudności w debuggowaniu.
Rozwiązanie:
W Go funkcja, która może zwrócić błąd, deklarowana jest w ten sposób:
func doSomething() (ResultType, error) { // ... if somethingWrong { return nil, errors.New("coś poszło nie tak") } return result, nil }
Sprawdzenie błędów jest obowiązkiem strony wywołującej:
res, err := doSomething() if err != nil { log.Fatalf("proces nie powiódł się: %w", err) }
Go 1.13 i wyżej pozwalają na "opakowywanie" błędów za pomocą fmt.Errorf("%w", err) do budowania łańcuchów błędów. Umożliwia to lepszą diagnozę i promuje dobre praktyki kontekstowego opisywania błędów.
Kluczowe cechy:
Po co tworzyć własne rodzaje błędów, skoro można po prostu zwracać errors.New("...")?
Poprawna odpowiedź: własne rodzaje błędów pozwalają nie tylko przekazać tekst błędu, ale także zachować dodatkowe informacje do dalszego przetwarzania (np. kody powrotu, kontekst), a także wdrożyć bardziej subtelną obsługę poprzez type assertion.
Przykład:
type NotFoundError struct { Resource string } func (e NotFoundError) Error() string { return fmt.Sprintf("%s nie znaleziono", e.Resource) } // Sprawdzenie if _, ok := err.(NotFoundError); ok { // obsługa błędu nieznalezienia }
Czy panic jest dobrą alternatywą dla błędów w przypadku błędów krytycznych?
Nie, panic w Go stosuje się tylko w rzeczywiście bezwyjściowych sytuacjach — na przykład, przy błędach samego programu (inwarianty programowe), ale nie dla zwykłych awarii (na przykład, nie udało się otworzyć pliku). Używanie panic do sygnalizowania rutynowych błędów jest złem i prowadzi do nieczytelnego, niezarządzalnego kodu.
Co się stanie, jeśli pominiesz obsługę błędu (err) w zagnieżdżonych funkcjach?
Błąd "ginie", kod nadal się wykonuje, co może prowadzić do rozprzestrzenienia się błędnego stanu. Zawsze ważne jest, aby prawidłowo obsługiwać każdy zwrot błędu.
_ = ...Każda funkcja po prostu zwraca errors.New(...), błędy nie są opakowane, rodzaje błędów są różne, ale obsługa zawsze jest ta sama — loguje się i rzuca. W rezultacie pliki logów są pełne nieinformatywnych wiadomości, które nie można śledzić do pierwotnej przyczyny.
Zalety:
Wady:
Używane są opakowania błędów za pomocą fmt.Errorf("%w", err), własne błędy z przydatnymi polami, sprawdzania za pomocą errors.Is()/errors.As(). Dzięki temu można logować szczegóły, oddzielać błędy biznesowe od awarii środowiska i pisać niezawodną logikę ponownego uruchamiania.
Zalety:
Wady: