programowanieProgramista Go

Jakie cechy dotyczące obsługi błędów w Go, jak poprawnie tworzyć i przetwarzać błędy, oraz jak realizować własne typy błędów?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź

Go od samego początku był zbudowany wokół jawnego zwracania błędów zamiast wyjątków. Umożliwia to rozwijanie przewidywalnego kodu i unikanie ukrytych pułapek charakterystycznych dla konstrukcji try/catch w innych językach (na przykład Java lub C++). Błąd w Go to interfejs, który implementuje metodę Error(), co pozwala na tworzenie zarówno prostych, jak i złożonych/owinętych błędów z kontekstem.

Problemy pojawiają się przy niewłaściwej obsłudze błędów (na przykład, jeśli są ignorowane przez _ lub nie są owinięte dodatkowymi informacjami do debugowania) lub tworzeniu "magicznych" błędów, które nie odpowiadają interfejsowi error. Ważne jest również, aby umieć zwracać błędy z publicznych funkcji i sprawdzać je w każdym wywołaniu.

Rozwiązanie — używać standardowych podejść:

  • Zawsze zwracaj błąd jako ostatnią wartość funkcji.
  • Używaj errors.New, fmt.Errorf do tworzenia błędów.
  • Dla błędów użytkownika twórz własne typy implementujące Error().
  • W przypadku złożonych przypadków stosuj błędy-owijki (errors.Wrap), aby nie tracić kontekstu.

Przykład kodu:

import ( "errors" "fmt" ) type NotFoundError struct { Resource string } func (e *NotFoundError) Error() string { return fmt.Sprintf("%s not found", e.Resource) } func GetUser(id int) (string, error) { if id != 1 { return "", &NotFoundError{"User"} } return "Steve", nil } func main() { user, err := GetUser(2) if err != nil { if nfe, ok := err.(*NotFoundError); ok { fmt.Println(nfe.Resource, "problem") } else { fmt.Println("error:", err) } } fmt.Println("user:", user) }

Kluczowe cechy:

  • Błędy zawsze są zwracane jawnie, zwykle jako ostatni argument funkcji.
  • Dla złożonych błędów można definiować własne typy (struct), implementujące interfejs error.
  • Do owijania błędów i zachowania śladu stosu można używać pakietu "errors" (Go 1.13+ obsługuje errors.Is i errors.As do pracy z łańcuchem błędów).

Pytania z podstępem.

Czy podczas obsługi błędu można porównywać przez ==, czy trzeba używać specjalnych metod?

Lepiej zawsze używać errors.Is() do porównywania z błędami-owijkami, w przeciwnym razie porównanie przez == może nie działać w przypadku istniejącej owijki błędu.

if errors.Is(err, os.ErrNotExist) { // obsługa braku pliku }

Czy obowiązkowe jest implementowanie typu-struct dla niestandardowego błędu, czy wystarczy używać errors.New("...")?

Jeśli potrzebny jest tylko tekst błędu, wystarczy errors.New(). Jeśli ważne jest zachowanie kontekstu (na przykład nazwy zasobu), lepiej zdefiniować struct z metodą Error().

Jak poprawnie zwrócić błąd w przypadku udanego wykonania funkcji?

W przypadku sukcesu zawsze zwracaj błąd nil.

return result, nil

Typowe błędy i antywzorce

  • Ignorowanie zwracanych błędów (przez _ lub pomijanie przetwarzania)
  • Porównywanie błędów tylko przez ==, mimo zagnieżdżonych błędów
  • Harwarded stringi zamiast błędów
  • Brak dodatkowego kontekstu w błędach

Przykład z życia

Negatywny przypadek

Programista pisze funkcję, która zwraca błąd bez wyjaśnień (po prostu errors.New("fail")). Przy analizie logów ustalenie przyczyny jest niemożliwe.

Zalety:

  • Szybkość implementacji

Wady:

  • Nieinformacyjne błędy
  • Trudności w debugowaniu

Pozytywny przypadek

Programista definiuje niestandardowy typ błędu NotFoundError i zwraca go z dokładnym opisem.

Zalety:

  • Wygoda filtrowania logów
  • Łatwość w wyszukiwaniu i przetwarzaniu błędów

Wady:

  • Potrzeba opisywania dodatkowych struktur