Often, we'll need to clean up resources, such as open files or open database connections. When our program is finished working with these resources, it's essential to close them to avoid memory leaks and allow other programs or applications to open and access them.
In this topic, we'll learn about one particular statement in Go that we can use to clean up resources during the execution of our program. It will help us make our code cleaner and less error-prone by keeping the calls to close the resource in proximity to the call that opened the resource previously.
The defer statement
In Go, we use the defer statement to delay the execution of a function until the surrounding function returns.
To further explain how the defer statement works, let's take a look at its most basic implementation:
package main
import "fmt"
func main() {
defer fmt.Println("Printed second! 2️⃣")
fmt.Println("Printed first! 1️⃣")
}
// Output:
// Printed first! 1️⃣
// Printed second! 2️⃣Notice that even though "Printed second! 2️⃣" is the first line in our program, it is not the first output! This is because any statement that is preceded by the defer statement isn't invoked until the end of the function in which defer was used.
Multiple defer statements
We can have many instances of the defer statement in our Go program. When there are multiple deferred statements, they are stored and executed as a stack.
The following example will illustrate how multiple defer statements and stacks work:
package main
import "fmt"
func main() {
defer fmt.Println("🐥") // first 1️⃣ deferred statement
defer fmt.Println("🐣") // second 2️⃣ deferred statement
defer fmt.Println("🥚") // third 3️⃣ deferred statement
}
// Output:
// 🥚
// 🐣
// 🐥In the above code, we first defer the defer fmt.Println("🐥"), which means it gets executed last. The defer fmt.Println("🥚"), which we defer last, is executed first.
In simple terms, each time we defer a statement, we push it onto a stack and then call it out in Last In, First Out (LIFO) order:
Defer with multiple functions
When working with multiple functions that have defer statements, it is important to note that the defer statement is scoped to the function inside which we declare it.
Let's go ahead and look at the following example, where we call the greeting() function from the main() function:
package main
import "fmt"
func greeting() {
defer fmt.Println("Printed after Hello, JB Academy!") // 2️⃣
fmt.Println("Hello, JB Academy!") // 1️⃣
}
func main() {
defer fmt.Println("Printed after the main() function is completed.") // 4️⃣
greeting()
fmt.Println("Printed after calling the greeting() function.") // 3️⃣
}
// Output:
// Hello, JB Academy!
// Printed after Hello, JB Academy!
// Printed after calling the greeting() function.
// Printed after the main() function is completed.After examining the code above, we can see that the deferred fmt.Println(...) statement within the greeting() function is executed once greeting is completed. The same occurs with the deferred fmt.Println(...) within the main() function after it is completed.
The following diagram helps us see more clearly the execution of the deferred statements within multiple functions:
In simple terms, when working with multiple functions that contain defer statements, the program executes every deferred statement within a function once that function it is contained within is completed or finished.
We can take a look at another example of scoped deferred statements below:
...
func scopedDefer() {
n := 0
defer func() { fmt.Println("n =", n, "- first deferred print") }()
{
defer func() { fmt.Println("n =", n, "- second deferred print") }()
n++ // n = 1
}
n++ // n = 2
}
// Output:
// n = 2 - second deferred print
// n = 2 - first deferred printWhen checking the output of our program, we can see that the deferred anonymous functions with fmt.Println(...) statements are only executed when the scopedDefer() function is completed. We can confirm this by checking the value of n = 2, which means that n was incremented two times, having its last increment just before the scopedDefer() function is completed.
Implementing defer to close a file
Although the previous examples show the order in which defer would be executed, they are not the most common ways we would use the defer statement within a Go program. The most standard implementation of defer is to clean up a resource such as an open file.
Let's go ahead and take a look at the use of the defer statement when writing a string to a file:
package main
import (
"fmt"
"log"
"os"
)
func main() {
file, err := os.Create("test.txt") // create and open 'test.txt' in read-and-write mode
if err != nil {
log.Fatal(err) // exit the program if we have an unexpected error
}
defer file.Close() // close the file before exiting the program
if _, err := fmt.Fprintln(file, "Hello World!"); err != nil {
log.Fatal(err)
}
}Pay attention to the defer file.Close() statement. It tells the compiler that it should close the file right after executing the fmt.Fprintln(...) function, just before the program is about to finish.
Do not worry right now about the code implementation of how to create a file and write a string to it! This is just an example of one of the most common applications of the defer statement in a Go program; we'll learn how to work with files in Go in future topics.
Deferring a call to a function such as file.Close() has two advantages. First, it guarantees that you will never forget to close a file, a mistake that's easy to make if we edit the function later to add a new return path. Second, it means that the file.Close() statement is in proximity to the os.Create() function, which is much clearer than placing it at the end of the main() function.
Summary
In this topic, we've learned about the defer statement. It helps us delay the execution of a function until the surrounding function returns. We've covered in detail the following theory:
The basic implementation and behavior of the
deferstatement in a Go program.With multiple
deferstatements, we push them onto a stack and execute them in Last In, First Out (LIFO) order.When working with multiple functions, the program executes every deferred statement within a function after that function is completed.
One of the most common use cases of the
deferstatement in Go is implementingdeferto close a file.
Now, to make sure you remember this theory and can implement the defer statement in your future Go programs, let's work on a few coding and theory problems.