Technical materials
Go programs use os.Exit
or log.Fatal*
to exit immediately (using panic
is not a good way to exit the program, please do not use panic).
Call one of os.Exit
or log.Fatal*
only in main()
. All other functions should return errors to the caller.
Not recommended:
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)
}
Recommended:
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
}
In principle, programs with multiple exit points have several issues:
- Obscure control flow: Any function can exit the program, making it difficult to reason about the control flow.
- Difficult to test: Functions that exit the program also exit the tests calling them. This makes the functions hard to test and introduces the risk of skipping other tests not yet run by
go test
. - Skipping cleanup: When a function exits the program, it skips any deferred function calls. This increases the risk of skipping important cleanup tasks.
One-time exit
If possible, there should only be a maximum of one call to os.Exit
or log.Fatal
in your main()
function. If there are multiple error scenarios that require stopping the program execution, place that logic in a separate function and return the error from there. This will shorten the main()
function and put all critical business logic in a separate, testable function.
Not recommended approach:
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 be executed.
b, err := os.ReadAll(f)
if err != nil {
log.Fatal(err)
}
// ...
}
Recommended approach:
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
}
// ...
}
The above example uses log.Fatal
, but this guideline also applies to os.Exit
or any library code that calls os.Exit
.
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
You may change the signature of run()
as needed. For example, if your program must exit with a specific failure code, run()
may return the exit code instead of an error. This also allows unit tests to directly validate this behavior.
func main() {
os.Exit(run(args))
}
func run() (exitCode int) {
// ...
}
Please note that the run()
function used in these examples is not compulsory. The name, signature, and setup of the run()
function are flexible. Among other things, you can:
- Accept unparsed command-line arguments (e.g.,
run(os.Args[1:])
) - Parse command-line arguments in
main()
and pass them torun
- Return the exit code back to
main()
with a custom error type - Put the business logic at a different abstraction level
package main
This guideline only requires that there is a place in main()
responsible for the actual exit flow.