Materiały techniczne

Programy Go używają os.Exit lub log.Fatal*, aby natychmiast zakończyć działanie (korzystanie z panic nie jest dobrym sposobem na zakończenie programu, proszę nie używać panic).

Wywołuj os.Exit lub log.Fatal* tylko w funkcji main(). Wszystkie inne funkcje powinny zwracać błędy do wywołującego.

Nie zalecane:

func main() {
  body := readFile(path)
  fmt.Println(body)
}
func readFile(path string) string {
  f, err := os.Open(path)
  if err != nil {
    log.Fatal(err)
  } 
  b, err := os.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  } 
  return string(b)
}

Zalecane:

func main() {
  body, err := readFile(path)
  if err != nil {
    log.Fatal(err)
  } 
  fmt.Println(body)
}
func readFile(path string) (string, error) {
  f, err := os.Open(path)
  if err != nil {
    return "", err
  } 
  b, err := os.ReadAll(f)
  if err != nil {
    return "", err
  } 
  return string(b), nil
}

Zasadniczo programy z wieloma punktami wyjścia mają kilka problemów:

  • Niejasny przepływ sterowania: Każda funkcja może zakończyć program, co utrudnia zrozumienie przepływu sterowania.
  • Trudne do przetestowania: Funkcje, które zamykają program, zamykają także testy, które je wywołują. Sprawia to, że testowanie funkcji jest trudne i wprowadza ryzyko pominięcia innych jeszcze nie wykonanych testów przez go test.
  • Pominięcie czyszczenia: Gdy funkcja zamyka program, pomija wywołania odroczone funkcji, zwiększając ryzyko pominięcia ważnych zadań czyszczenia.

Jednorazowe wyjście

Jeśli to możliwe, w funkcji main() powinno być maksymalnie jedno wywołanie os.Exit lub log.Fatal. Jeśli występuje wiele scenariuszy błędów, które wymagają zatrzymania wykonywania programu, umieść tę logikę w osobnej funkcji i zwróć błąd stamtąd. Spowoduje to skrócenie funkcji main() i umieszczenie wszystkiej istotnej logiki biznesowej w osobnej, testowalnej funkcji.

Nie zalecane podejście:

package main
func main() {
  args := os.Args[1:]
  if len(args) != 1 {
    log.Fatal("brak pliku")
  }
  name := args[0]
  f, err := os.Open(name)
  if err != nil {
    log.Fatal(err)
  }
  defer f.Close()
  // Jeśli wywołamy log.Fatal po tej linii
  // f.Close zostanie wykonane.
  b, err := os.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }
  // ...
}

Zalecane podejście:

package main
func main() {
  if err := run(); err != nil {
    log.Fatal(err)
  }
}
func run() error {
  args := os.Args[1:]
  if len(args) != 1 {
    return errors.New("brak pliku")
  }
  name := args[0]
  f, err := os.Open(name)
  if err != nil {
    return err
  }
  defer f.Close()
  b, err := os.ReadAll(f)
  if err != nil {
    return err
  }
  // ...
}

Przykład powyżej używa log.Fatal, ale ta zasada dotyczy również os.Exit lub dowolnego kodu bibliotecznego wywołującego os.Exit.

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

Możesz zmienić sygnaturę run() według potrzeb. Na przykład, jeśli Twój program musi zakończyć się z określonym kodem błędu, run() może zamiast błędu zwracać kod wyjścia. Pozwala to także na bezpośrednie sprawdzenie tego zachowania w testach jednostkowych.

func main() {
  os.Exit(run(args))
}

func run() (exitCode int) {
  // ...
}

Należy zauważyć, że funkcja run() użyta w tych przykładach nie jest obowiązkowa. Nazwa, sygnatura i konfiguracja funkcji run() są elastyczne. Między innymi można:

  • Zaakceptować nieprzetworzone argumenty wiersza poleceń (np. run(os.Args[1:]))
  • Parsować argumenty wiersza poleceń w main() i przekazywać je do run
  • Zwracać kod wyjścia do main() za pomocą niestandardowego typu błędu
  • Umieścić logikę biznesową na innym poziomie abstrakcji package main

Ta zasada wymaga jedynie istnienia miejsca w main(), które odpowiada za rzeczywisty przepływ zakończenia.