技術資料

Goプログラムでは、os.Exitlog.Fatal* を使用して、直ちに終了します(panic を使用するのはプログラムを終了させる良い方法ではないため、panic を使用しないでください)。

main() 関数の中でのみ os.Exitlog.Fatal* のいずれかを呼び出すことが推奨されます。他のすべての関数はエラーを呼び出し元に返すべきです。

非推奨:

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

推奨:

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
}

原則として、複数の終了ポイントを持つプログラムにはいくつかの問題があります:

  • 不明瞭な制御フロー:どの関数でもプログラムを終了できるため、制御フローを理解することが難しくなります。
  • テストが難しい:プログラムを終了する関数は、それを呼び出すテストも終了させます。これにより、関数をテストすることが難しくなり、go test でまだ実行されていない他のテストをスキップするリスクが生じます。
  • クリーンアップのスキップ:関数がプログラムを終了すると、遅延実行される関数呼び出しもスキップされます。これにより、重要なクリーンアップタスクをスキップするリスクが高まります。

一度だけの終了

可能であれば、main() 関数での os.Exitlog.Fatal の呼び出しは最大1回となるべきです。プログラムの実行を停止する複数のエラーシナリオがある場合は、そのロジックを別の関数に置き、そこからエラーを返すようにします。これにより main() 関数が短くなり、すべての重要なビジネスロジックを別の、テスト可能な関数にまとめることができます。

非推奨アプローチ:

package main
func main() {
  args := os.Args[1:]
  if len(args) != 1 {
    log.Fatal("missing file")
  }
  name := args[0]
  f, err := os.Open(name)
  if err != nil {
    log.Fatal(err)
  }
  defer f.Close()
  // この行の後に log.Fatal を呼び出すと、f.Close が実行されます。
  b, err := os.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }
  // ...
}

推奨されるアプローチ:

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("missing file")
  }
  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
  }
  // ...
}

上記の例では log.Fatal を使用していますが、このガイドラインは os.Exit または os.Exit を呼び出す任意のライブラリコードにも適用されます。

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

必要に応じて run() のシグネチャを変更することができます。たとえば、プログラムが特定の失敗コードで終了する必要がある場合は、run() がエラーの代わりに終了コードを返すようにすることができます。これにより、ユニットテストで直接この動作を検証することも可能になります。

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

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

これらの例で使用されている run() 関数は必須ではないことに注意してください。run() 関数の名前、シグネチャ、セットアップは柔軟です。他のことの中で、次のことができます:

  • パースされていないコマンドライン引数を受け入れる(例:run(os.Args[1:])
  • main() でコマンドライン引数をパースして、run に渡す
  • 終了コードをカスタムエラータイプと共に main() に返す
  • ビジネスロジックを異なる抽象化レベルの別の場所 package main に配置する

このガイドラインは、実際の終了フローに責任を持つ main() 内の場所があることだけを要求します。