Specyfikacja Obsługi Błędów w Golang

Typy Błędów

Istnieje kilka opcji deklaracji błędów. Przed wyborem najlepszej opcji do swojego przypadku użycia, rozważ następujące kwestie:

  • Czy wywołujący potrzebuje dopasować błąd, aby go obsłużyć? Jeśli tak, musimy obsłużyć funkcje errors.Is lub errors.As, deklarując zmienne błędów na najwyższym poziomie lub niestandardowe typy.
  • Czy komunikat błędu to stały ciąg znaków czy dynamiczny ciąg znaków wymagający informacji kontekstowej? Dla stałych ciągów znaków możemy użyć errors.New, ale dla ostatniego musimy użyć fmt.Errorf lub niestandardowego typu błędu.
  • Czy przekazujemy nowe błędy zwracane przez funkcje zależne? Jeśli tak, należy odnieść się do sekcji opakowywania błędów.
Dopasowanie błędu? Komunikat błędu Wskazówki
Nie statyczny errors.New
Nie dynamiczny fmt.Errorf
Tak statyczny zmienna na najwyższym poziomie z errors.New
Tak dynamiczny niestandardowy typ error

Na przykład, użyj errors.New, aby reprezentować błędy ze stałymi ciągami znaków. Jeśli wywołujący potrzebuje dopasować i obsłużyć ten błąd, wyeksportuj go jako zmienną, aby umożliwić dopasowanie za pomocą errors.Is.

Brak dopasowania błędu

// pakiet foo

func Open() error {
  return errors.New("nie można otworzyć")
}

// pakiet bar

if err := foo.Open(); err != nil {
  // Nie można obsłużyć błędu.
  panic("nieznany błąd")
}

Dopasowanie błędu

// pakiet foo

var ErrCouldNotOpen = errors.New("nie można otworzyć")

func Open() error {
  return ErrCouldNotOpen
}

// pakiet bar

if err := foo.Open(); err != nil {
  if errors.Is(err, foo.ErrCouldNotOpen) {
    // obsłuż błąd
  } else {
    panic("nieznany błąd")
  }
}

Dla błędów z dynamicznymi ciągami znaków, użyj fmt.Errorf, jeśli wywołujący nie musi go dopasować. Jeśli wywołujący musi go dopasować, użyj niestandardowego typu error.

Brak dopasowania błędu

// pakiet foo

func Open(file string) error {
  return fmt.Errorf("plik %q nie znaleziony", file)
}

// pakiet bar

if err := foo.Open("testfile.txt"); err != nil {
  // Nie można obsłużyć błędu.
  panic("nieznany błąd")
}

Dopasowanie błędu

// pakiet foo

type NotFoundError struct {
  File string
}

func (e *NotFoundError) Error() string {
  return fmt.Sprintf("plik %q nie znaleziony", e.File)
}

func Open(file string) error {
  return &NotFoundError{File: file}
}


// pakiet bar

if err := foo.Open("testfile.txt"); err != nil {
  var notFound *NotFoundError
  if errors.As(err, &notFound) {
    // obsłuż błąd
  } else {
    panic("nieznany błąd")
  }
}

Należy zauważyć, że jeśli wywołujesz zmienne błędów lub typy z pakietu, stają się one częścią publicznego interfejsu API pakietu.

Pakowanie błędów

Kiedy występuje błąd podczas wywoływania innej metody, zazwyczaj istnieją trzy sposoby jego obsługi:

  • Zwróć oryginalny błąd bez zmian.
  • Użyj fmt.Errorf z %w, aby dodać kontekst do błędu, a następnie go zwróć.
  • Użyj fmt.Errorf z %v, aby dodać kontekst do błędu, a następnie go zwróć.

Jeśli nie ma dodatkowego kontekstu do dodania, zwróć oryginalny błąd bez zmian. To zachowa oryginalny typ błędu i jego komunikat. Jest to szczególnie odpowiednie, gdy podstawowa wiadomość o błędzie zawiera wystarczająco informacji do śledzenia jego źródła.

W przeciwnym razie, dodaj jak najwięcej kontekstu do komunikatu o błędzie, aby nie występowały niejednoznaczne błędy, takie jak "connection refused". Zamiast tego otrzymasz bardziej użyteczne błędy, np. "wywoływanie usługi foo: connection refused".

Użyj fmt.Errorf, aby dodać kontekst do swoich błędów i wybierz pomiędzy zmiennymi %w lub %v w zależności od tego, czy wywołujący ma możliwość dopasowania i wydobycia prawdziwej przyczyny.

  • Użyj %w, jeśli wywołujący powinien mieć dostęp do podstawowego błędu. Jest to dobre domyślne zachowanie dla większości pakowanych błędów, ale miej świadomość, że wywołujący może zacząć polegać na tej funkcjonalności. Dlatego dla pakowanych błędów, które są znanymi zmiennymi lub typami, zapisz je i przetestuj jako część kontraktu funkcji.
  • Użyj %v, aby zasłonić podstawowy błąd. Wywołujący nie będzie w stanie go dopasować, ale w przyszłości możesz przełączyć się na %w, jeśli zajdzie taka potrzeba.

Dodając kontekst do zwracanego błędu, unikaj używania fraz typu "failed to", aby utrzymać zwięzły kontekst. Gdy błąd przenika przez stos, będzie się on nakładać warstwowo:

Nie zalecane:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "failed to create new store: %w", err)
}

// failed to x: failed to y: failed to create new store: the error

Zalecane:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "new store: %w", err)
}
// x: y: new store: the error

Jednakże, gdy błąd zostaje przesłany do innego systemu, powinno być jasne, że komunikat jest błędem (np. etykieta "err" lub prefiks "Failed" w logach).

Nieprawidłowe Nazewnictwo

Dla wartości błędów przechowywanych jako zmienne globalne, użyj przedrostka Err lub err w zależności od tego, czy są one wyeksportowane. Proszę odnieść się do wytycznych. Dla niewyeksportowanych stałych i zmiennych na najwyższym poziomie, użyj podkreślenia (_) jako przedrostka.

var (
  // Wyeksportuj poniższe dwa błędy, tak aby użytkownicy tego pakietu mogli je dopasować za pomocą errors.Is.
  ErrBrokenLink = errors.New("link is broken")
  ErrCouldNotOpen = errors.New("could not open")

  // Ten błąd nie jest eksportowany, ponieważ nie chcemy, aby był częścią naszego publicznego API. Możemy go nadal używać w obrębie pakietu za pomocą errors.
  errNotFound = errors.New("not found")
)

Dla niestandardowych typów błędów, użyj przyrostka Error.

// Podobnie, ten błąd jest eksportowany, tak aby użytkownicy tego pakietu mogli dopasować go za pomocą errors.As.
type NotFoundError struct {
  File string
}

func (e *NotFoundError) Error() string {
  return fmt.Sprintf("file %q not found", e.File)
}

// Ten błąd nie jest eksportowany, ponieważ nie chcemy, aby był częścią publicznego API. Możemy go nadal używać w obrębie pakietu za pomocą errors.As.
type resolveError struct {
  Path string
}

func (e *resolveError) Error() string {
  return fmt.Sprintf("resolve %q", e.Path)
}

Obsługa błędów

Gdy wywołujący otrzymuje błąd od wywołanej funkcji, może on obsługiwać błąd różnymi sposobami w zależności od zrozumienia błędu.

Obejmuje to, ale nie ogranicza się do:

  • Dopasowywanie błędu za pomocą errors.Is lub errors.As, jeśli wywołana funkcja zgodziła się na określenie konkretnego błędu, oraz obsługa rozgałęzień w różny sposób
  • Logowanie błędu i łagodne degradowanie, jeśli błąd jest odwracalny
  • Zwracanie dobrze zdefiniowanego błędu, jeśli reprezentuje on warunek awarii związany z domeną
  • Zwracanie błędu, czy to owiniętego czy w postaci oryginalnej

Niezależnie od sposobu, w jaki wywołujący obsługuje błąd, zazwyczaj powinien on obsłużyć każdy błąd tylko raz. Na przykład wywołujący nie powinien logować błędu, a następnie go zwracać, ponieważ jego wywołujący może również obsłużyć błąd.

Na przykład rozważmy następujące scenariusze:

Nieodpowiednie: Logowanie błędu i jego zwracanie

Inne wywołujące wyżej w stosie mogą podjąć podobne działania w przypadku tego błędu. Spowoduje to duże zanieczyszczenie w dziennikach aplikacji przy niewielkiej korzyści.

u, err := getUser(id)
if err != nil {
  // ZŁE: Patrz opis
  log.Printf("Nie można pobrać użytkownika %q: %v", id, err)
  return err
}

Dobre: Owijanie błędu i jego zwracanie

Błędy wyżej w stosie będą obsługiwać ten błąd. Użycie %w zapewnia, że będą mogły dopasować błąd za pomocą errors.Is lub errors.As, jeśli jest to istotne.

u, err := getUser(id)
if err != nil {
  return fmt.Errorf("pobierz użytkownika %q: %w", id, err)
}

Dobre: Logowanie błędu i łagodne degradowanie

Jeśli operacja nie jest absolutnie konieczna, możemy zapewnić łagodne degradowanie poprzez jego obsługę bez przerywania doświadczenia.

if err := emitMetrics(); err != nil {
  // Błąd w zapisywaniu metryk nie powinien
  // przerywać działania aplikacji.
  log.Printf("Nie można wysłać metryk: %v", err)
}

Dobre: Dopasowywanie błędu i odpowiednie łagodne degradowanie

Jeśli wywołana funkcja określiła konkretny błąd w swojej umowie i awaria jest odwracalna, dopasuj ten przypadek błędu i łagodnie go degraduj. W pozostałych przypadkach owiń błąd i zwróć go. Błędy wyżej w stosie będą obsługiwać inne błędy.

tz, err := getUserTimeZone(id)
if err != nil {
  if errors.Is(err, ErrUserNotFound) {
    // Użytkownik nie istnieje. Użyj UTC.
    tz = time.UTC
  } else {
    return fmt.Errorf("pobierz użytkownika %q: %w", id, err)
  }
}

Obsługa niepowodzeń asercji

Asertywność typów spowoduje awarię z pojedynczą wartością zwracaną w przypadku nieprawidłowego wykrycia typu. Dlatego zawsze używaj również idiomy "przecinek, ok".

Nie zalecane:

t := i.(string)

Zalecane:

t, ok := i.(string)
if !ok {
  // Obsłuż błąd łagodnie
}

Unikaj używania panic

Kod uruchamiany w środowisku produkcyjnym powinien unikać użycia panic. Panic jest głównym źródłem kaskadowych awarii. Jeśli wystąpi błąd, funkcja powinna zwrócić ten błąd i pozwolić wywołującemu zdecydować, jak go obsłużyć.

Niezalecane:

func run(args []string) {
  if len(args) == 0 {
    panic("wymagany jest argument")
  }
  // ...
}

func main() {
  run(os.Args[1:])
}

Zalecane:

func run(args []string) error {
  if len(args) == 0 {
    return errors.New("wymagany jest argument")
  }
  // ...
  return nil
}

func main() {
  if err := run(os.Args[1:]); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

panic/recover nie jest strategią obsługi błędów. Należy używać panic tylko w przypadku wystąpienia nieodwracalnego zdarzenia (np. odwołanie do nila). Wyjątkiem jest inicjalizacja programu: sytuacje, które mogą spowodować panic, powinny być obsługiwane podczas uruchamiania programu.

var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))

Nawet w kodzie testowym zaleca się używanie t.Fatal lub t.FailNow zamiast panic, aby zapewnić oznaczenie awarii.

Niezalecane:

// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "test")
if err != nil {
  panic("nie udało się skonfigurować testu")
}

Zalecane:

// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "test")
if err != nil {
  t.Fatal("nie udało się skonfigurować testu")
}