Go by Example

Exit

Keep cleanup in a run function; report errors from main and exit nonzero after cleanup completes.

os.Exit terminates a process immediately with a status code. Deferred functions do not run, so call it only at the program boundary after cleanup decisions are complete.

A run function can return errors while its deferred cleanup still runs. main reports the error and chooses the process exit code.

package main
 
import (
    "errors"
    "fmt"
    "os"
)
 
func run() error {
    if len(os.Args) < 2 {
        return errors.New("usage: program <name>")
    }
    fmt.Println("hello,", os.Args[1])
    return nil
}
 
func main() {
    if err := run(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

In production

Use zero for success and a non-zero status for failure so shells and CI can detect errors. Avoid log.Fatal inside a running service because it also calls os.Exit and skips deferred cleanup. Keep exit decisions in main or another top-level command boundary.

Enjoyed this? Get more software craft delivered to your inbox.