defer checkClose

The defer keyword is one of those little features that makes Go such a great language, and makes the life of Go developers easier.

defer schedules a function call to execute right before the innermost enclosing function returns. It is somewhat similar to the try/finally construct in Java. Once a function call is deferred, it is guaranteed to execute, regardless of the path execution takes upon exitting out of the enclosing function, including panics.

Classic examples for using defer are closing a resource, unlocking a mutex, and recovering from a panic.

func (d *Daiquiri) Price() int {
	d.mutex.Lock()
	defer d.mutex.Unlock()

	return d.price
}

The example above shows a getter function safe for concurrent access. The lock is acquired with the first statement, and then defer schedules calling Unlock() to release the lock upon the function’s return. Next, the return statement is executed to evaluate the value for the function and end its execution. Finally, any deferred calls are executed in reverse order, in this case Unlock() to release the lock.

The same example could be written by putting the statements in the order of they should execute: lock, load price, unlock, return price. Using defer however groups related statements together (locking and unlocking the mutex). In a more complex function with multiple return paths, it also avoids having to release the lock on every path out of the function. This leads to simpler, more linear and more readable code, and it makes it easier to reason about correctness.

The technique I want to introduce with this post focuses on closing resources such as files. Just like in the previous example, defer can be used to guarantee the resource is closed regardless of what path the execution takes out of the function:

func Handle() error {
	f, err := os.Open("file.txt")
	if err != nil {
		return err
	}
	defer f.Close()

	// do something with f
	return nil
}

Unfortunately, there is a slight issue with the code above (can you spot it?). Closing the resource could return an error, which may mean the function did not complete successfully (e.g., data was not flushed to the disk) and should in turn return an error. defer however ignores the return value for f.Close(), and Handle returns nil.

This can be fixed by deferring an anonymous function and using named return values for the Handle function:

func Handle() (errOut error) {
	f, err := os.Open("file.txt")
	if err != nil {
		return err
	}
	defer func() {
		if err := f.Close(); err != nil && errOut == nil {
			errOut = err
		}
	}()
	
	// do something with f
	return nil
}

The example above works by overwriting the return value of the Handle function right before it returns, doing so from the deferred closure. Note that errOut is only modified if it is nil, otherwise the previous error value is preserved. Also note that despite the use of return values names, no naked return statements were used (naked returns are bad practice even in short functions).

This code works correctly, but it’s starting to feel clunky due to the closure and multiple indentation levels. Code can additionally become repetitive if you’re using this pattern in multiple places. This becomes especially true for functions using multiple resources, e.g., a routine that copies from an input file to one or more output files.

The code above can be refactored as follows:

func Handle() (errOut error) {
	f, err := os.Open("file.txt")
	if err != nil {
		return err
	}
	defer checkClose(f, &errOut)
	
	// do something with f
	return nil
}

func checkClose(c io.Closer, err *error) {
	if err2 := c.Close(); *err == nil {
		*err = err2
	}
}

Here, closing the resource and updating the error value has been moved into a separate, small function named checkClose. The Handle function maintains the readability of the original code, by deferring a simple call to checkClose instead of using the closure. A pointer must be used to pass errOut down the call stack, otherwise checkClose would only update its own local copy of errOut instead of the return value of Handle as intended.

Finally, note the odd condition *err == nil in checkClose, which ensures existing errors are not overwritten. An extra check on err2 is not necessary, as even when err2 is nil, overwriting nil with nil has no effect.


A similar technique was shown a while ago on goinggo.net.