1

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?

5
  • It just means to call os.Exit just one in func main. How you do that (e.g. via run()) doesn't matter and is not important to that rule. Commented Mar 23, 2022 at 10:12
  • 2
    If you've been instructed, you should know what it means to your employer. If you've just been told, ask for instructions. Commented Mar 23, 2022 at 10:20
  • The intention of the guideline is to produce code that handles fatal errors in one place. In the example you've cited, 3 log.Fatal calls get replaced by a single log.Fatal Commented Mar 23, 2022 at 13:49
  • @Volker The rationale behind Uber's guideline is more profound than you think. Commented Mar 25, 2022 at 7:49
  • @byxor The intention isn't simply to reduce the number of calls to log.Fatal. See my answer. Commented Mar 25, 2022 at 7:50

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.