Go Defer
Introduction
The defer
statement is one of Go's distinctive features that helps developers write cleaner, more maintainable code. When you mark a function call with the defer
keyword, Go schedules that function to be executed just before the surrounding function returns. This powerful mechanism simplifies resource management, error handling, and cleanup operations that would otherwise require complex try-finally blocks in other languages.
In this tutorial, we'll explore how the defer
statement works, its execution order, common use cases, and best practices.
Basic Syntax and Behavior
The syntax for using defer
is straightforward:
defer functionCall()
Here's a simple example:
package main
import "fmt"
func main() {
fmt.Println("Start")
defer fmt.Println("This will be printed last")
fmt.Println("End")
}
Output:
Start
End
This will be printed last
In this example, even though the deferred function call appears second in the code, it executes last—after the surrounding function (main
) completes all its other operations but before it actually returns.
Key Characteristics of Defer
1. LIFO Execution Order (Last In, First Out)
When multiple defer
statements appear in a function, they execute in reverse order (like a stack):
package main
import "fmt"
func main() {
fmt.Println("Counting:")
for i := 1; i <= 3; i++ {
defer fmt.Println(i)
}
fmt.Println("Done counting")
}
Output:
Counting:
Done counting
3
2
1
Notice how the numbers are printed in reverse order (3, 2, 1) because the last deferred statement is executed first.
2. Arguments Are Evaluated Immediately
Although the deferred function isn't executed until the surrounding function returns, any arguments to the deferred function are evaluated immediately when the defer
statement is encountered:
package main
import "fmt"
func main() {
x := 1
defer fmt.Println("x =", x) // x evaluated as 1 here
x = 2
fmt.Println("Current x =", x)
}
Output:
Current x = 2
x = 1
Even though x
was changed to 2 before the deferred function executed, the deferred function still prints "x = 1" because the value was captured when the defer
statement was encountered.
Common Use Cases
1. Resource Cleanup
The most common use of defer
is to ensure resources are properly released, regardless of how a function exits:
package main
import (
"fmt"
"os"
)
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // File will always be closed when function exits
// Read and process the file...
fmt.Println("Reading file:", filename)
return nil
}
func main() {
err := readFile("example.txt")
if err != nil {
fmt.Println("Error:", err)
}
}
In this example, file.Close()
is guaranteed to be called before readFile()
returns, whether it completes normally or encounters an error during file processing.
2. Mutex Unlocking
defer
is ideal for ensuring mutexes are unlocked:
package main
import (
"fmt"
"sync"
"time"
)
func safeAccess(m *sync.Mutex) {
m.Lock()
defer m.Unlock() // Ensures mutex is unlocked even if panic occurs
// Critical section
fmt.Println("Accessing shared resource...")
time.Sleep(time.Second) // Simulate work
fmt.Println("Done with shared resource")
}
func main() {
mutex := &sync.Mutex{}
safeAccess(mutex)
}
3. Timing Function Execution
You can use defer
to measure function execution time:
package main
import (
"fmt"
"time"
)
func timeTrack(start time.Time, name string) {
elapsed := time.Since(start)
fmt.Printf("%s took %s
", name, elapsed)
}
func expensiveOperation() {
defer timeTrack(time.Now(), "expensiveOperation")
// Simulate work
fmt.Println("Performing expensive operation...")
time.Sleep(2 * time.Second)
}
func main() {
expensiveOperation()
}
Output:
Performing expensive operation...
expensiveOperation took 2.000123456s
4. Panic Recovery
defer
plays a crucial role in Go's panic recovery mechanism:
package main
import "fmt"
func recoverExample() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from:", r)
}
}()
fmt.Println("About to panic")
panic("something went wrong")
fmt.Println("This line will never execute")
}
func main() {
recoverExample()
fmt.Println("Program continues running")
}
Output:
About to panic
Recovered from: something went wrong
Program continues running
Practical Examples
Example 1: Database Connection Management
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"log"
)
func queryDatabase() {
// Open doesn't actually establish a connection
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close() // Close connection when function exits
// Now use the database
rows, err := db.Query("SELECT name FROM users LIMIT 10")
if err != nil {
log.Fatal(err)
}
defer rows.Close() // Close the rows when done
// Process rows...
fmt.Println("Processing database results...")
}
Example 2: HTTP Request with Proper Body Closing
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func fetchWebpage(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close() // Always close the response body
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
func main() {
content, err := fetchWebpage("https://example.com")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Website length:", len(content))
}
Example 3: Using Defer for Cleanup in a Processing Pipeline
package main
import (
"fmt"
"os"
)
func processData(filename string) error {
// Open the input file
inputFile, err := os.Open(filename)
if err != nil {
return fmt.Errorf("failed to open input file: %w", err)
}
defer inputFile.Close()
// Create temporary file
tempFile, err := os.Create(filename + ".tmp")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
defer func() {
tempFile.Close()
// Clean up temp file if something goes wrong
os.Remove(tempFile.Name())
}()
// Process data and write to temp file...
fmt.Println("Processing data...")
// Create output file
outputFile, err := os.Create(filename + ".processed")
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer outputFile.Close()
// Finalize processing...
fmt.Println("Data processing complete!")
return nil
}
func main() {
err := processData("data.txt")
if err != nil {
fmt.Println("Error:", err)
}
}
Best Practices and Common Pitfalls
Do's:
- Use defer for cleanup operations: Like closing files, network connections, and database resources.
- Keep deferred functions simple: Avoid complex logic in deferred functions.
- Place defer statements near the resource acquisition: This improves code readability.
Don'ts:
- Don't defer in loops for long-running programs: Each defer statement consumes memory until the function returns.
// BAD: This could lead to memory issues in long loops
func processLargeDataset(filenames []string) {
for _, filename := range filenames {
file, err := os.Open(filename)
if err != nil {
continue
}
defer file.Close() // Will only close after ALL files are processed!
// Process file...
}
}
// GOOD: Close each file within the loop
func processLargeDataset(filenames []string) {
for _, filename := range filenames {
func() {
file, err := os.Open(filename)
if err != nil {
return
}
defer file.Close() // Closes when this anonymous function returns
// Process file...
}()
}
}
- Be careful with deferred functions that modify named return values:
func example() (result int) {
defer func() { result *= 2 }() // Modifies the return value
return 5
}
// This returns 10, not 5!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)