defer is a keyword in Go that allows developers to schedule a function call to be executed when the surrounding function returns. This powerful feature guarantees that the deferred function will run on every exit path—whether the function completes normally or exits due to a panic. The concept is similar to RAII in C++ or try–finally in Java: cleanup or finalization logic is guaranteed to run when control leaves a scope. In this article, I’ll explain how defer works, explore common usage patterns, and highlight frequent pitfalls—and how to avoid them.
Let's start with a simple example to illustrate how defer works in Go. Consider the following code snippet:
func readFile(filename string) (string, error) {
file, err := os.Open(filename)
if err != nil {
return "", err
}
buffer := make([]byte, 100)
_, err = file.Read(buffer)
if err != nil {
file.Close()
return "", err
}
file.Close()
return string(buffer), nil
}In this example, we open a file and read its contents. However, if an error occurs during the read operation, we must remember to close the file before returning. This can lead to code duplication and potential resource leaks if we forget to close the file in every error path. In this case it is easy to spot all the return paths, but in more complex functions it can be easy to miss one. To address this issue, we can use defer to ensure that the file is closed when the function exits, regardless of how it exits:
func readFile(filename string) (string, error) {
file, err := os.Open(filename)
if err != nil {
return "", err
}
defer file.Close()
buffer := make([]byte, 100)
_, err = file.Read(buffer)
if err != nil {
return "", err
}
return string(buffer), nil
}In this revised version, we use defer file.Close() immediately after opening the file. This ensures that the file will be closed when the function returns, regardless of whether it returns due to an error or completes successfully. This not only simplifies the code but also enhances its readability and maintainability.
Above we saw a basic usage of defer, but there are some important rules and best practices to keep in mind when using it. Here are three important rules to keep in mind when using defer in Go:
This means that if you have multiple defer statements in a function, they will be executed in reverse order(LIFO) of their appearance when the function exits.
func lifo() {
fmt.Println("LIFO")
defer fmt.Println("First Deferred")
defer fmt.Println("Second Deferred")
defer fmt.Println("Third Deferred")
}
// LIFO
// Third Deferred
// Second Deferred
// First DeferredWhen using defer, keep in mind that all arguments—including the receiver—are evaluated right away, not when the deferred function runs. The example below demonstrates this:
func captureByValue() {
file := "file1.txt"
defer printFileContent(file)
file = "file2.txt"
}In this example, file is evaluated immediately when the defer statement is encountered. That means when printFileContent is eventually called, it still refers to "file1.txt", even though file was later reassigned to "file2.txt". This behavior can feel unintuitive at first, but it’s deliberate. When you reuse a variable to open multiple files, deferring Close ensures that each file is closed correctly—at the point it was opened—rather than closing only the last one.
However, if you want to avoid this behaviour there are two common approaches:
This means wrapping the deferred function call inside another function. That way, you capture the variable by reference, not by value like before. Variables referenced by a defer closure are evaluated during the closure execution(hence, when the surrounding function returns). The implementation looks like this:
func captureByReferenceClosure() {
file := "file1.txt"
defer func() {
printFileContent(file)
}()
file = "file2.txt"
}
// content file2.txtBy passing a pointer to the variable, you ensure that the deferred function accesses the current value of the variable when it executes. However, usually using a closure is more idiomatic in Go. Here’s how you can implement this approach:
func captureByReferencePointer() {
file := "file1.txt"
defer printFileContent(&file)
file = "file2.txt"
}If a function has named return values, deferred functions can access and modify those values before the function actually returns. This can be useful for setting return values based on cleanup operations or error handling. Popular example for this is in the case when we want to capture errors from the deferred function and return them:
type File struct {
Name string
}
func Open(name string) (*File, error) {
file := &File{Name: name}
return file, nil
}
func (f *File) Close() error {
return fmt.Errorf("Error closing file: %s", f.Name)
}
// Here we use the err named return value to capture errors from close
// the errors.Join function combines the error from the main function and the error from Close
func correctErrorHandling() (err error) {
file, err := Open("nonexistent.txt")
if err != nil {
return err
}
defer func() {
err = errors.Join(err, file.Close())
}()
return fmt.Errorf("Random error")
}
// Random error
// Error closing file: nonexistent.txtIn Go, when a panic occurs, the normal execution flow is interrupted, and the program starts unwinding the stack. The only way to recover from a panic is by using the recover function, which can only be called within a deferred function. This is true, because deferred functions are guaranteed to run when a function exits, even if it exits due to a panic. Here’s an example that demonstrates how defer, panic, and recover work together:
func foo() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("no worries, recovered from panic: %v", r)
}
}()
panic("trigger panic for demonstration")
}
// no worries, recovered from panic: trigger panic for demonstration}To wrap up, here I want to highlight 2 popular mistakes that newcomers to Go often make, when they start using defer:
A common mistake is to place the defer statement before checking for errors when opening a resource. If the resource fails to open, the deferred function will attempt to Close non-existent resource, which is unwanted behavior. Always ensure that the resource is successfully opened before deferring its closure.
func wrongErrorHandling() {
file, err := os.Open("nonexistent.txt")
defer file.Close() // wrong, should be below error check
if err != nil {
fmt.Println("Error opening file:", err)
return
}
}
func correctErrorHandling() {
file, err := os.Open("nonexistent.txt")
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer file.Close() // correct
}Another common mistake is to place defer statements inside loops, since deferred functions are executed when the surrounding function returns. This means that if you defer a function inside a loop, all the deferred calls will accumulate and only execute after the entire function completes. This can lead to excessive resource usage and potential memory leaks.
func wrongReadFiles(files []string) error {
for file := range files {
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close() // Close will not be called till the end of the function
}
return nil
}There are 2 popular ways to avoid this issue:
By using an anonymous function, you can ensure that resources are properly closed at the end of each iteration.
func correctReadFilesClosure(files []string) error {
for _, file := range files {
err := func() error {
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close()
return nil
}()
if err != nil {
return err
}
}
return nil
}By delegating resource management to a separate function, you can ensure that resources are properly closed after each operation. This is more popular way to handle this situation, as it leads to cleaner and more maintainable code. It's more readable than using closures in most cases. Furthermore we can test the helper function independently.
func readFile(file string) {
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close()
}
func correctReadFiles(files []string) error {
for file := range files {
readFile(file)
}
return nil
}I hope after reading this article, that you have a deeper understanding of how defer works in Go. It is a powerful feature that, when used correctly, can greatly enhance the readability and maintainability of your code. By following best practices and being aware of common pitfalls, you can leverage defer to write cleaner and more efficient Go programs.