defer
Programs often create temporary resources, like files or network connections, that need to be cleaned up. This cleanup has to happen, no matter how many exit points a function has, or whether a function completed successfully or not. 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. We’ll do this by writing a simple version of cat, the Unix utility for printing the contents of a file. We can’t open files on The Go Playground, but you can find the code for this example on GitHub in the simple_cat directory:
func main() { if len(os.Args) < 2 { log.Fatal(“no file specified”) } f, err := os.Open(os.Args[1]) if err != nil { log.Fatal(err) } 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 { log.Fatal(err) } break } } }
This example introduces a few new features that we cover in more detail in later chapters. Feel free to read ahead to learn more.
First, we make sure that a file name was specified on the command line by checking the length of os.Args, a slice in the os package that contains the name of the program launched and the arguments passed to it. If the argument is missing, we use the Fatal function in the log package to print a message and exit the program. Next, we acquire a read-only file handle with the Open function in the os package. The second value that’s returned by Open is an error. If there’s a problem opening the file, we print the error message and exit the program. As mentioned earlier, we’ll talk about errors in Chapter 8.
Once we know we have a valid file handle, we need to close it after we use it, no matter how we exit the function. To ensure the cleanup code runs, we use the defer keyword, followed by a function or method call. In this case, we use the Close method on the file variable. (We look at methods in Go in Chapter 7.) Normally, a function call runs immediately, but defer delays the invocation until the surrounding function exits.
We read from a file handle by passing a slice of bytes into the Read method on a file variable. We’ll cover how to use this method in detail in “io and Friends”, but Read returns the number of bytes that were read into the slice and an error. If there’s an error, we check to see if it’s an end-of-file marker. If we are at the end of the file, we use break to exit the for loop. For all other errors, we report it and exit immediately using log.Fatal. We’ll talk a little more about slices and function parameters in “Go Is Call By Value” and go into details on this pattern when we discuss pointers in the next chapter.
Building and running the program from within the simple_cat directory produces the following result:
$ go build $ ./simple_cat simple_cat.go package main import ( “fmt” “os” ) …
There are a few more things that you should know about defer. First, you can defer multiple closures in a Go function. They run in last-in-first-out order; the last defer registered runs first.
The code within defer closures runs after the return statement. As I mentioned, you can supply a function with input parameters to a defer. Just as defer doesn’t run immediately, any variables passed into a deferred closure aren’t evaluated until the closure runs.
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 if 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 us to take actions based on an error. When we talk about errors in Chapter 8, we will discuss a pattern that uses a defer to add contextual information to an error returned from a function. Let’s look at a way to handle database transaction cleanup 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 return nil }
We’re not going to cover Go’s database support in this book, but the standard library includes extensive support for databases in the database/sql package. In our example function, we create a transaction to do a series of database inserts. If any of them fails, we want to roll back (not modify the database). If all of them succeed, we want to commit (store the database changes). We use a closure with defer to check if err has been assigned a value. If it hasn’t, we run a tx.Commit(), which could also return an error. If it does, the value err is modified. If any database interaction returned an error, we call tx.Rollback().
Note
New Go developers tend to forget the parentheses when specifying a closure for defer. It is a compile-time error to leave them out and eventually the habit sets in. It helps to remember that supplying parentheses allows you to specify values that will be passed into the closure when it runs.
A common pattern in Go is for a function that allocates a resource to also return a closure that cleans up the resource. In the simple_cat_cancel directory in our GitHub project, there is a rewrite of our simple cat program that does this. First we write a helper function that opens a file and returns a closure:
func getFile(name string) (*os.File, func(), error) { file, err := os.Open(name) if err != nil { return nil, nil, err } return file, func() { file.Close() }, nil }
Our helper function returns a file, a function, and an error. That * means that a file reference in Go is a pointer. We’ll talk more about that in the next chapter.
Now in main, we use our getFile function:
f, closer, err := getFile(os.Args[1]) if err != nil { log.Fatal(err) } 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 is not called. That reminds the user to use defer. As we covered earlier, you 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 when a resource is cleaned up, 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, and that 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, Miroslaw Staron, and Anna Sandberg discovered that “Of…eleven proposed code characteristics, only two markedly influence complexity growth: the nesting depth and the lack of structure.”
Research on what makes a program easier to read and understand isn’t new. You can find papers that are many decades old, including a paper from 1983 by Richard Miara, Joyce Musseman, Juan Navarro, and Ben Shneiderman that tries to figure out the right amount of indentation to use (according to their results, two to four spaces).