一架梯子,一头程序猿,仰望星空!
Golang开发规范 > 内容正文

指导原则-主函数退出方式


Go 程序使用 os.Exit 或者 log.Fatal* 立即退出 (使用panic不是退出程序的好方法,请 不要使用 panic。)

仅在main() 中调用其中一个 os.Exit 或者 log.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 尚未运行的其他测试的风险。
  • 跳过清理:当函数退出程序时,会跳过已经进入defer队列里的函数调用。这增加了跳过重要清理任务的风险。

一次性退出

如果可能的话,你的main()函数中 最多一次 调用 os.Exit或者log.Fatal。如果有多个错误场景停止程序执行,请将该逻辑放在单独的函数下并从中返回错误。 这会缩短 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()函数的名称、签名和设置具有灵活性。除其他外,您可以:

  • 接受未分析的命令行参数 (e.g., run(os.Args[1:]))
  • 解析main()中的命令行参数并将其传递到run
  • 使用自定义错误类型将退出代码传回main()
  • 将业务逻辑置于不同的抽象层 package main

本指南只要求在main()中有一个位置负责实际的退出流程。