...

Golang Defer: A Practical Guide with 5 Use Cases

Rate this post

Introduction

Golang defer provides a mechanism to postpone the execution of a function until the surrounding function finishes its execution. It allows for clean and efficient resource management, ensuring that specified functions are executed just before a function returns, regardless of the return reason—whether it’s normal execution, panic, or an error.

Defer in Go is particularly useful for tasks such as closing files, releasing locks, or executing cleanup operations, ensuring that resources are properly managed and that any necessary actions are taken before exiting a function’s scope. This feature streamlines code readability and reliability by centralizing cleanup operations, minimizing the risk of resource leaks or errors due to unfinished operations.

The defer statement, with its simplicity and flexibility, contributes significantly to writing cleaner and more maintainable Go code, enhancing the reliability and robustness of programs by ensuring proper handling of resources and cleanup tasks.

What is Golang defer?

Defer applies to a function call in go.
defer statement is used to schedule a function call to be executed when the surrounding function completes its execution, regardless of whether it finishes normally or due to an error.
It ensures that certain tasks(unlocking, clean up, etc.) are executed at the end of a given function.

Let us understand defer in go with an example.

Suppose, we want to make an HTTP request to fetch some data

type Student struct {
	Name string `json:"name"`
	Age int `json:"name"`
}

func getStudents(url string) ([]Student, error) {
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}

	var students []Student

	err = json.NewDecoder(resp.Body).Decode(&students)
	if err != nil {
		return nil, err
	}
	resp.Body.Close() // buggy code

	return students, nil
}

If JSON decode fails in the above code snippet then we would return without closing the response body which would ultimately cause a memory leak. Also, we might even forget to close the response body, which will result in the same memory leak and it is a bad coding practice.

We can solve both problems with the use of defer in go as below.

type Student struct {
	Name string `json:"name"`
	Age int `json:"name"`
}

func getStudents(url string) ([]Student, error) {
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close() // it will always close response body
	
	var students []Student

	err = json.NewDecoder(resp.Body).Decode(&students)
	if err != nil {
		return nil, err
	}

	return students, nil
}

Let us understand defer in golang with below mind-map

golang defer
Golang Defer

The execution order of defer in Golang

The defer statement is executed in the Last In, First Out (LIFO) order. Multiple defer statements within a function are stacked, and their corresponding functions are called in the reverse order of their deferral.

Let us understand the LIFO order of defer in go with the below example:-

package main

import (
	"fmt"
)

func main() {
	defer fmt.Println("Deferred statement 1")
	defer fmt.Println("Deferred statement 2")
	defer fmt.Println("Deferred statement 3")
	fmt.Println("Main function body")
}

// Output
Main function body
Deferred statement 3
Deferred statement 2
Deferred statement 1

The evaluation order of defer in Golang

In Go, the evaluation order of deferred functions involves a subtle yet important consideration. When deferring a function call, the arguments of the deferred function are evaluated immediately, but the execution of that function is deferred until the surrounding function exits.

Consider the following example:

package main

import (
	"fmt"
)

func main() {
	i := 0

	defer fmt.Println("Deferred output:", i) // i is evaluated immediately

	i++
	fmt.Println("Regular output:", i)
}

In this scenario, the output will be:

Regular output: 1
Deferred output: 0

The key observation is that when the defer statement is encountered, the i variable’s current value (which is 0) is immediately evaluated and stored, but the execution of the deferred function occurs after the main function completes. Therefore, the deferred function prints the value of i as it existed when the defer statement was encountered, not its current value upon execution. Understanding this order of evaluation is crucial for ensuring expected behavior when using defer in Go.

Major use cases of defer in Golang

1. Resource Cleanup

Defer is used to release resources like HTTP response body(illustrated in the above example), file handles (for reading/writing to files), and network connections (websocket connections). Here, defer will ensure that resources are released in all conditions.
Let us understand with the below examples:

1 do any operation on files

func anyOperationOnFile() error {
    file := openFile("data.txt")
    defer file.Close() // Close the file when processFile() returns, regardless of how it returns

    // Process the file
    // ...

    return nil
}


2 close websocket connection
func wsHandler(w http.ResponseWriter, r *http.Request) {
  conn, err := upgrader.Upgrade(w, r, nil)
  if err != nil {
    log.Errorln("Failed to upgrade connection:", err)
    return
  }
  // make sure to close the connection before exit
 defer conn.Close()

// do other tasks
}


3 close http response body

func getStudents(url string) ([]Student, error) {
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close() // it will always close response body
	
	var students []Student

	err = json.NewDecoder(resp.Body).Decode(&students)
	if err != nil {
		return nil, err
	}

	return students, nil
}

2. Unlocking Mutexes

Mutexes must be unlocked after being locked otherwise other go routines can’t access it

func processSyncTask() {
    mutex.Lock()
    defer mutex.Unlock() // Ensure mutex is always unlocked

    // Perform critical operations
    // ...
}

3. Logging and Tracing

defer can be used to log or trace function calls and their execution time. By deferring a logging statement at the beginning of a function, we can ensure it is always executed upon function exit.

func expensiveOperation() {
    defer logTimeTaken(time.Now(), "expensiveOperation") // Log the execution time

    // Perform expensive operation
    // ...
}

4. Panic Recovery

defer can be used to recover from panics and handle them gracefully. By using defer in go a function that invokes the recover built-in function, you can capture and handle panics.

func handlePanic() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()

    // Code that may panic
    // ...
}

5. WaitGroup update

Waitgroups are used to execute many independent tasks asynchronously and the caller waits for their completion. Let us understand the use of defer in go for such task functions with the below example

func logTasks() {
  var wg sync.WaitGroup
       wg.Add(3)
       go task1(&wg)
       go task2(&wg)
       go task3(&wg)
       wg.Wait()
     
    /// other tasks
}

func task1(wg *sync.WaitGroup) {
  defer wg.Done()
 // do task
}

func task2(wg *sync.WaitGroup) {
  defer wg.Done()
 // do task
}

func task3(wg *sync.WaitGroup) {
  defer wg.Done()
 // do task
}

Performance impact of defer in go

In Go, using defer statements incur a slight performance impact due to the additional function call and stack management overhead. Although minimal, this impact becomes notable in scenarios involving a high volume of deferred functions, potentially affecting performance-sensitive applications.

The deferred functions, being queued for execution upon the function exit, contribute to a marginal increase in memory consumption and can slightly slow down execution compared to direct function calls. While the impact is typically negligible in most cases, careful consideration of defer usage in performance-critical sections or loops is advisable to maintain optimal performance in Go applications

Conclusion

In conclusion, defer in Go serves as a powerful tool for ensuring efficient resource management and cleanup within code. By allowing functions to be deferred until the end of a function’s execution, ‘defer’ facilitates the orderly handling of tasks such as closing files, releasing locks, or executing cleanup actions, regardless of the function’s return status.

The beauty of ‘defer’ lies in its simplicity and reliability, making code more readable and maintainable. It centralizes cleanup operations, reducing the chance of resource leaks and promoting more resilient code. This mechanism enhances the overall robustness of Go programs by ensuring that necessary actions are performed before leaving a function’s scope.

The ‘defer’ statement, with its straightforward yet impactful nature, aligns with Go’s philosophy of simplicity and efficiency, contributing significantly to writing cleaner, more organized, and error-resilient codebases, thereby elevating the quality and reliability of Go applications.

Enjoy the post!

Related Posts

Spread the love

Leave a Comment

Seraphinite AcceleratorOptimized by Seraphinite Accelerator
Turns on site high speed to be attractive for people and search engines.