在主函数中,为什么我应该最多只调用一次os.Exit函数?

huangapple go评论135阅读模式
英文:

Why should I call os.Exit at most once in the main function?

问题

我开始了一份新工作,我们被要求使用Uber的Go编码规范。我对其中一项指南"Exit Once"不太确定:

如果可能的话,在main()函数中最多只调用一次os.Exitlog.Fatal。如果有多个导致程序终止的错误场景,将该逻辑放在一个单独的函数中,并从该函数返回错误。

这难道不意味着将main()函数转移到另一个函数中(比如run())吗?这对我来说似乎有点多余。Uber的这种做法有什么好处呢?

英文:

I started a new job and we've been instructed to use Ubers Go coding standards. I'm not sure about one of their guidelines entitled "Exit Once":

> If possible, prefer to call os.Exit or log.Fatal at most once in your main(). If there are multiple error scenarios that halt program execution, put that logic under a separate function and return errors from it.

Wouldn't this just mean offloading main() into another function (run())? This seems a little superfluous to me. What benefits does Uber's approach have?

答案1

得分: 2

我不熟悉Uber的完整Go编码规范,但是那个建议是正确的。os.Exit的一个问题是它会非常粗暴地结束程序,而不会执行任何待处理的延迟函数调用:

Exit会以给定的状态码退出当前程序。按照惯例,代码零表示成功,非零表示错误。
程序立即终止;延迟函数不会被运行

(我强调的部分)

然而,这些延迟函数调用可能负责重要的清理任务。考虑Uber的示例代码片段:

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 := ioutil.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }

  // ...
}

如果ioutil.ReadAll返回一个非nil的错误,将调用log.Fatal;而且因为log.Fatal在底层调用了os.Exit,延迟调用f.Close将不会被执行。在这种特定情况下,这并不是很严重,但是想象一下,如果延迟调用涉及一些清理工作,比如删除文件,那么你会让你的磁盘处于一个不干净的状态。关于这个主题的更多信息,请参见Go Time podcast的第112集,其中讨论了这些考虑因素。

因此,在程序中尽量避免使用os.Exitlog.Fatal等方法是一个好主意。Uber的Go编码规范中描述的run函数允许在程序执行结束之前按照预期运行延迟调用(可能带有非零状态码):

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 := ioutil.ReadAll(f)
  if err != nil {
    return err
  }

  // ...
}

这种方法的另一个好处是,虽然main函数本身不容易进行测试,但你可以设计一个具有可测试性的run函数;关于这个主题,请参见Mat Ryer的博文为什么不应该在Golang中使用func main

英文:

I'm not familiar with Uber's entire Go coding standards, but that particular piece of advice is sound. One issue with os.Exit is that it puts an end to the programme very brutally, without honouring any deferred function calls pending:

> Exit causes the current program to exit with the given status code. Conventionally, code zero indicates success, non-zero an error.
> The program terminates immediately; deferred functions are not run.

(my emphasis)

However, those deferred function calls may be responsible for important cleanup tasks. Consider Uber's example code snippet:

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

  // If we call log.Fatal after this line,
  // f.Close will not be called.

  b, err := ioutil.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }

  // ...
}

If ioutil.ReadAll returns a non-nil error, log.Fatal is called; and because log.Fatal calls os.Exit under the hood, the deferred call to f.Close will not be run. In this particular case, it's not that serious, but imagine a situation where deferred calls involved some cleanup, like removing files; you'd leave your disk in an unclean state. For more on that topic, see episode #112 of the Go Time podcast, in which these considerations were discussed.

Therefore, it's a good idea to eschew os.Exit, log.Fatal, etc. "deep" in your programme. A run function as described in Uber's Go coding standards allows deferred calls to be run as they should before programme execution ends (potentially with a non-zero status code):

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 := ioutil.ReadAll(f)
  if err != nil {
    return err
  }

  // ...
}

An additional benefit of this approach is that, although the main function itself isn't readily testable, you can design such a run function with testability in mind; see Mat Ryer's blog post on that topic.

huangapple
  • 本文由 发表于 2022年3月23日 18:08:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/71585332.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定