notexOPENOverview
Notex/Languages/Cleaning up with defer
03.14LESSON 14 OF 18 · 7 MIN READ

Cleaning up with defer

Always free to read

Programs often create temporary resources, like files or network connections, that need to be cleaned up, no matter how many exit points the function has, or whether it completes successfully. In Go, the cleanup code is attached to the function with the defer keyword.

Let’s take a look at how to use defer to release resources. You’ll do this by writing a simple version of cat, the Unix utility for printing the contents of a file. You can’t pass in arguments on the Go Playground, but you can find the code for this example in the examples/simple_cat directory. This example introduces a few new features that I’ll cover in more detail in later chapters:

func main() {
    if len(os.Args) < 2 {
        slog.Error("no file specified")
        os.Exit(1)
    }
    os.Exit(run())
}

func run() int {
    f, err := os.Open(os.Args[1])
    if err != nil {
        slog.Error("open failed", slog.Any("err", err))
        return 1
    }
    defer f.Close()
    data := make([]byte, 2048)
    for {
        count, err := f.Read(data)
        os.Stdout.Write(data[:count])
        if err != nil {
            if err != io.EOF {
                slog.Error("read failed", slog.Any("err", err))
                return 1
            }
            break
        }
    }
    return 0
}

First, the program ensures that a filename is specified on the command line by checking the length of os.Args, a slice in the os package. The first value in os.Args is the program name. The remaining values are the arguments passed to the program. Check that the length of os.Args is at least 2 to determine whether the argument was provided to the program. If it wasn’t, the program will call the Error function in the log/slog package to print a message, and end execution using os.Exit(1) to indicate that it has exited due to an error.

Next, the run function is called. In run, the program tries to acquire a read-only file handle via the Open function in the os package. The second value returned by Open is an error. If there’s a problem opening the file, it will print the error message and return 1.

Once you know there is a valid file handle, you need to close it after you use it, no matter how you exit the function. To ensure that the cleanup code runs, use the defer keyword followed by a function or method call. In this case, you’ll use the Close method on the file variable. Normally, a function call would run immediately, but defer delays its invocation until the surrounding function exits.

You can read from a file handle by passing a slice of bytes to the Read method on a file variable. I’ll cover how to use this method in detail in the related discussion, but for now, know that Read returns the number of bytes that were read into the slice, along with an error. If an error occurs, check whether it’s an end-of-file marker. If you’re at the end of the file, use break to exit the for loop. Report all other errors using slog.Error and exit run with a value of 1. I’ll talk a little more about slices and function parameters in “Go Is Call by Value” and will go into more detail on this pattern when I discuss pointers in this topic.

If the file is read to the end, run returns 0.

Finally, the flow of execution returns to main, and the value returned from run is passed to os.Exit. A 0 indicates the program has exited normally; a 1 indicates that an error has occurred.

Building and running the program from within the simple_cat directory produces the following result:

$ go build
$ ./simple_cat main.go
package main

import (
    "io"
    "log/slog"
    "os"
)
...

Tip

Why is most of this program outside of main? It’s because of defer. By convention, a program that exits due to an error returns a nonzero exit code. In Go, you indicate this with the os.Exit function. The problem is that calling os.Exit immediately exits the program, which means that any deferred functions are skipped. To ensure that deferred functions run, only call os.Exit from main, and do not use defer in main if there’s an os.Exit. If you move most of the logic to run, the program can use defer to close resources properly and return the correct exit code on error.

You should know a few more things about defer. First, you can use a function, method, or closure with defer. (When I mention functions with defer, you can mentally expand that to “functions, methods, or closures.”)

You can defer multiple functions in a Go function. They run in last-in, first-out (LIFO) order; the last defer registered runs first.

The code within defer functions runs after the return statement. As I mentioned, you can supply a function with input parameters to a defer. The input parameters are evaluated immediately and their values are stored until the function runs.

Here’s a quick example that demonstrates both of these features of defer:

func deferExample() int {
    a := 10
    defer func(val int) {
        fmt.Println("first:", val)
    }(a)
    a = 20
    defer func(val int) {
        fmt.Println("second:", val)
    }(a)
    a = 30
    fmt.Println("exiting:", a)
    return a
}

Running this code gives the following output:

exiting: 30
second: 20
first: 10

You can run this on The Go Playground. It is also available in the examples/defer_example directory.

Note

You can supply a function that returns values to a defer, but there’s no way to read those values:

func example() {
    defer func() int {
        return 2 // there's no way to read this value
    }()
}

You might be wondering whether there’s a way for a deferred function to examine or modify the return values of its surrounding function. There is, and it’s the best reason to use named return values. It allows your code to take actions based on an error. When I talk about errors in the related discussion>, I will discuss a pattern that uses defer to add contextual information to an error returned from a function.

Let’s look at a way to clean up database transactions using named return values and defer:

func DoSomeInserts(ctx context.Context, db *sql.DB, value1, value2 string)
                  (err error) {
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    defer func() {
        if err == nil {
            err = tx.Commit()
        }
        if err != nil {
            tx.Rollback()
        }
    }()
    _, err = tx.ExecContext(ctx, "INSERT INTO FOO (val) values $1", value1)
    if err != nil {
        return err
    }
    // use tx to do more database inserts here with value2
    return nil
}

I’m not going to cover Go’s database support in these notes, but the standard library includes extensive support for databases in the database/sql package. In the example function, you create a transaction to do a series of database inserts. If any of them fails, roll back rather than modifying the database. If they all succeed, commit (store the database changes). Use a closure with defer to check whether err has been assigned a value. If it hasn’t, run tx.Commit. This could also return an error. If it does, the value err is modified. If any database interaction returns an error, call tx.Rollback.

Note

New Go developers tend to forget the set of parentheses that goes after the braces when specifying a function for defer. It is a compile-time error to leave them out, so eventually the habit sets in. It helps to remember that supplying parentheses allows you to specify values that will be passed into the function when it runs.

Another common pattern in Go: a function that allocates a resource can also return a closure that cleans up the resource. In the examples/simple_cat_cancel directory, there is a rewrite of the simple cat program that does this. First you write a helper function that opens a file and returns a closure:

func openFile(name string) (*os.File, func(), error) {
    file, err := os.Open(name)
    if err != nil {
        return nil, nil, err
    }
    return file, func() {
        file.Close()
    }, nil
}

The helper function returns a file, a function, and an error. That * means that a file reference in Go is a pointer. I’ll talk more about that in this topic.

Now, in run, use the openFile function:

f, closer, err := openFile(os.Args[1])
if err != nil {
    slog.Error("cannot open file", slog.Any("err", err))
    return 1
}
defer closer()

Because Go doesn’t allow unused variables, returning the closer from the function means that the program will not compile if the function isn’t called. That reminds the user to use defer. As covered earlier, put parentheses after closer when you defer it.

Note

Using defer can feel strange if you are used to a language that uses a block within a function to control resource cleanup, like the try/catch/finally blocks in Java, JavaScript, and Python or the begin/rescue/ensure blocks in Ruby.

The downside to these resource-cleanup blocks is that they create another level of indentation in your function, which makes the code harder to read. It’s not just my opinion that nested code is harder to follow: In research described in a 2017 paper in Empirical Software Engineering, Vard Antinyan et al. discovered that “Of …​ eleven proposed code characteristics, only two markedly influence complexity growth: the nesting depth and the lack of structure.”

There is a wide body of research on what makes a program easier to read and understand. You can find papers many decades old, including a 1983 paper by Richard Miara et al. that tries to figure out the right amount of indentation to use (according to their results, two to four spaces).

Progress is saved on this device.
KEEP EXPLORING

One note leads to another.

Back to the library →
⌕
Search across the entire library ⌘ K