Technische Unterlagen

Go-Programme verwenden os.Exit oder log.Fatal*, um sofort zu beenden (das Verwenden von panic ist keine gute Methode, um das Programm zu beenden, bitte verwenden Sie kein panic).

Rufen Sie os.Exit oder log.Fatal* nur in main() auf. Alle anderen Funktionen sollten Fehler an den Aufrufer zurückgeben.

Nicht empfohlen:

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)
}

Empfohlen:

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
}

Im Allgemeinen haben Programme mit mehreren Beendigungspunkten mehrere Probleme:

  • Undurchsichtiger Kontrollfluss: Jede Funktion kann das Programm beenden, was es schwierig macht, den Kontrollfluss nachzuvollziehen.
  • Schwierig zu testen: Funktionen, die das Programm beenden, beenden auch die Tests, die sie aufrufen. Dies macht die Funktionen schwer zu testen und birgt das Risiko, dass noch nicht von go test ausgeführte andere Tests übersprungen werden.
  • Auslassen von Aufräumarbeiten: Wenn eine Funktion das Programm beendet, werden alle ausstehenden Funktionsaufrufe übersprungen. Dies erhöht das Risiko, wichtige Aufräumaufgaben zu überspringen.

Einmaliger Ausstieg

Idealerweise sollte es höchstens einen Anruf von os.Exit oder log.Fatal in Ihrer main()-Funktion geben. Wenn es mehrere Fehlerfälle gibt, die das Beenden der Programmausführung erfordern, platzieren Sie diese Logik in einer separaten Funktion und geben Sie den Fehler von dort zurück. Dadurch wird die main()-Funktion verkürzt und die gesamte kritische Geschäftslogik wird in einer separaten, testbaren Funktion platziert.

Nicht empfohlener Ansatz:

package main
func main() {
  args := os.Args[1:]
  if len(args) != 1 {
    log.Fatal("fehlende Datei")
  }
  name := args[0]
  f, err := os.Open(name)
  if err != nil {
    log.Fatal(err)
  }
  defer f.Close()
  // Wenn wir log.Fatal nach dieser Zeile aufrufen
  // f.Close wird ausgeführt.
  b, err := os.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }
  // ...
}

Empfohlener Ansatz:

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("fehlende Datei")
  }
  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
  }
  // ...
}

Das obige Beispiel verwendet log.Fatal, aber diese Richtlinie gilt auch für os.Exit oder jeglichen Bibliothekscode, der os.Exit aufruft.

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

Sie können die Signatur von run() nach Bedarf ändern. Wenn Ihr Programm z.B. mit einem bestimmten Fehlercode beendet werden muss, kann run() den Beendigungscode anstelle eines Fehlers zurückgeben. Dies ermöglicht auch, dass Unit-Tests dieses Verhalten direkt validieren.

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

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

Bitte beachten Sie, dass die Verwendung der run()-Funktion in diesen Beispielen nicht verbindlich ist. Der Name, die Signatur und die Einrichtung der run()-Funktion sind flexibel. Unter anderem können Sie:

  • Unanalysierte Befehlszeilenargumente akzeptieren (z. B. run(os.Args[1:]))
  • Befehlszeilenargumente in main() analysieren und an run übergeben
  • Den Beendigungscode mit einem benutzerdefinierten Fehlertyp zurück an main() geben
  • Die Geschäftslogik auf einer anderen Abstraktionsebene package main platzieren

Diese Richtlinie erfordert lediglich, dass es in main() einen Ort gibt, der für den tatsächlichen Beendigungsvorgang verantwortlich ist.