Question

Should I use log.Fatalf in my main function?

I'm using Golang to write a script that produces a file. At the moment, I'm testing the process with the following code:

func main() {

    var builder strings.Builder
    keys := []int{1, 2, 3, 4}
    for _, key := range keys {
        // Add Iso3166 templates
        builder.WriteString(fmt.Sprintf("This is key %v", key)
        builder.WriteString("\n\n")
    }

    file, err := os.Create(fileName)
    if err != nil {
        log.Fatalf("Failed to create file: %v", err)
    }
    defer file.Close()
    _, err = file.Write([]byte(standardBeginning))
    if err != nil {
        log.Fatalf("Failed to write iso3166: %v", err)
    }
}

This code works, but I get the following warning in the IDE:

log.Fatalf will exit, and `defer file.Close()` will not run

I've read around to see what I should do about this, particularly this question here: Should a Go package ever use log.Fatal and when?. But I'm still not sure if log.Fatalf is reasonable to use in my situation.

Am I in danger of leaving my file open? Is there a better way to handle errors like this?

 2  90  2
1 Jan 1970

Solution

 8

The function log.FatalF calls os.Exit with a non-zero exit status.

Calling os.Exit with a non-zero status is the preferred way to indicate that the process was not successful.

Deferred functions are not executed when the program exits with os.Exit. It's OK to exit the program in the question without running the deferred functions, but it may be important to run the deferred functions in other programs.

To run the deferred functions, move the body of main to another function and return an error from that function. Call log.Fatal from main.

func run() {
    var builder strings.Builder
    keys := []int{1, 2, 3, 4}
    for _, key := range keys {
        // Add Iso3166 templates
        builder.WriteString(fmt.Sprintf("This is key %v", key)
        builder.WriteString("\n\n")
    }

    file, err := os.Create(fileName)
    if err != nil {
        return fmt.Errorf("Failed to create file: %w", err)
    }
    defer file.Close()
    _, err = file.Write([]byte(standardBeginning))
    if err != nil {
        return fmt.Errorf("Failed to write iso3166: %w", err)
    }
    return nil
}

func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}
2024-07-22
Kimberly Cheatle

Solution

 0

Your IDE is correct, that once you call log.Fatalf, no deferred functions will run.

However...

Am I in danger of leaving my file open?

No. Because log.Fatalf exits the program. A program that's not running can't hold any files open, obviously.

BUT! In other cases, you may have defered statements that are important to run before exiting. A very common example would be a logger that needs to be flushed before exiting. Imagine a scenario like this:

func main() {
    logger := MyImportantLogger()
    defer logger.Close() // Ensures that all logs are written before exiting
    conf, err := SomeFunctionToReadConfiguration()
    if err != nil {
        log.Fatalf("Invalid configuration found: %s", err)
    }
    /* The rest of your program */
}

In this case, you're likely to never see the "Invalid configuration" error, as the program will exit before the logger has a chance to ensure that all logs have been properly sent/recorded.

So, in summary, it's usually a good idea to avoid log.Fatalf, or more broadly, os.Exit() (which is called by log.Fatalf), except when you're absolutely sure that all cleanup has been done.

2024-07-22
Jonathan Hall