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 dorun
- 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.