Defer You Should Know in Golang(Fundamental Applications)

Go’s defer: The Secret to Elegant Code

Your friends will know you better in the first minute you meet than your acquaintances will know you in a thousand years.
— Richard Bach

Medium Link: Defer You Should Know in Golang(Fundamental Applications) | by Wesley Wei | Sep, 2024 | Programmer’s Career
Author:Wesley Wei – LinkedIn, Wesley Wei – Medium, Wesley Wei – Twitter

golang defer(from github.com/MariaLetta/free-gophers-pack) |300

Hello, here is Wesley, Today’s article is about defer in Go. Without further ado, let’s get started.💪

1.1 Use Cases

  1. Resource Cleanup: For example, closing files, database links, or network links.
1
2
3
4
5
6
7
8
func readFile(filename string) {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
defer file.Close()
// do something
}
  1. Unlocking Resources: For example, ensuring locks are unlocked or executing wg.Done in concurrent programming.
1
2
3
4
5
6
7
8
9
10
11
12
var mu sync.Mutex

func criticalSection() {
mu.Lock()
defer mu.Unlock()
// do something
}

func worker(wg *sync.WaitGroup, id int) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
}
  1. Ensuring post-function operations: such as logging or debugging information, performance analysis, and more, can be handled reliably at the end of function execution.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"time"
)

// Records the execution time of a function
func timeTrack(start time.Time, name string) {
elapsed := time.Since(start)
fmt.Printf("%s took %s\n", name, elapsed)
}

// Logs and tracks the execution of the function
func someFunction() {
defer timeTrack(time.Now(), "someFunction") // Records the function's execution time
defer fmt.Println("Function completed") // Logs the completion of the function

fmt.Println("Executing someFunction…")
time.Sleep(2 * time.Second)
// Simulating multiple return points
if true {
return // Due to defer, the cleanup or logging will still execute after this return
}
fmt.Println("This line won't be reached")
}

func main() {
someFunction()
}

It greatly simplifies code writing and maintenance, reducing the risk of resource leaks and logical errors. However, it does come with a learning curve, making it worth our time to explore in depth. This time, I plan to start with the official documentation to understand its basic usage and the philosophy behind it. Let’s dive in!

1.2 Triggering Time

A “defer” statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement[1],reached the end of its function body[2], or because the corresponding goroutine is panicking[3].
From: https://go.dev/ref/spec#Defer_statements[4]

  1. executed a return statement[5]
  2. reached the end of its **function body[6]
  3. corresponding goroutine is panicking[7]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func deferFn1() {
defer fmt.Println("defer")
fmt.Println("func")
}
// output:
// func
// defer
func deferFn2() {
defer fmt.Println("defer")
return
}
// output:
// defer
func deferFn3() {
defer fmt.Println("defer")
panic("boom")
}
// output:
// defer
// panic stack

In Go, the defer statement is executed just before the surrounding function returns. This means that regardless of how the function exits—whether through a normal return or due to a panic—the execution of the defer statement is guaranteed, as long as it was declared before the return or panic occurs.

1.3 Execution Order and Key Considerations

  1. The expression must be a function or method call; it cannot be parenthesized. Calls of built-in functions are restricted as for expression statements[8].
  2. Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usual[9] and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred.
    From: https://go.dev/ref/spec#Defer_statements[10]

In terms of the execution order of defer statements, Go has a crucial feature: stack-like invocation (LIFO - Last In, First Out). This means that if multiple defer statements are defined consecutively in a function, the last defer statement will be executed first.

1
2
3
4
5
6
7
8
9
10
11
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("end…")
}
// output
end…
3
2
1

Additionally, defer has an important characteristic that must be understood: the function value and parameters to the call are evaluated as usual[11] and saved anew but the actual function is not invoked.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
startedAt := time.Now()
fmt.Println(time.Since(startedAt))
time.Sleep(time.Second)
}
// output
0s

func main() {
startedAt := time.Now()
defer func() {
fmt.Println(time.Since(startedAt))
}()
time.Sleep(time.Second)
}
// output
1s

We can approach these two examples from two different perspectives:

  1. Timing of “Capture”:
  • In the first example, when the function is deferred, its arguments are immediately evaluated at the time the defer statement is invoked, not when the deferred function is actually executed. This means that at the moment the defer is declared, the value of startedAt is “captured,” even though the function itself runs later.
  • In the second example, we see that defer can also be applied to closures (anonymous functions). A closure captures variables from its surrounding lexical scope at the time it is executed. Therefore, the value of startedAt is “captured” when the deferred function runs, not when it’s declared.
  1. Value Copying:
  • The value copy in the first example is straightforward and easy to understand.
  • In the second example, we can think of it this way: the anonymous function is essentially a closure. It captures and references variables from its external scope because when you use the defer keyword with an anonymous function, Go does not copy the entire function’s content. Instead, it stores a pointer to that function and calls it later when necessary.
  • Thus, we can interpret it as copying a function pointer, and the external variables are resolved when the function is eventually invoked.

I prefer the second interpretation, as it deepens my understanding of value copying—plus, it spares me from having to remember so many “pseudo-concepts.”

1.4 Return Value Handling

  1. if the surrounding function returns through an explicit return statement[12], deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller. 
  2. if the deferred function is a function literal and the surrounding function has named result parameters that are in scope within the literal, the deferred function may access and modify the result parameters before they are returned 
  3. If the deferred function has any return values, they are discarded when the function completes
    From: https://go.dev/ref/spec#Defer_statements[13]

1.4.1 Basic Logic

if the surrounding function returns through an explicit return statement[14], deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller.

1
2
3
4
5
6
7
8
9
func deferReturnFn() int {
a := 1
defer func() {
a = 2
}()
return a
}
// output
1

Isn’t it strange that the result returns as 1? Can you find the reason from the first sentence of the official documentation? I noticed that defer and return operations are not atomic. To simplify understanding, you can assume that the compiler essentially redefines a variable to hold the current return value before executing the return statement:

1
2
3
4
5
6
7
8
func deferReturnFn() int {
a := 1
defer func() {
a = 2
}()
res := a
return res
}

So you can understand why the result is 1.

1.4.2 Named Result Parameters

If the deferred function is a function literal and the surrounding function has named result parameters that are in scope within the literal, the deferred function may access and modify the result parameters before they are returned

The second sentence emphasizes the case of Named Result Parameters, as shown in the following example, which outputs 2.

1
2
3
4
5
6
7
8
9
func deferReturnFn() (res int) {
res = 1
defer func() {
res = 2
}()
return
}
// output
2

At first glance, it might seem quite strange. However, compared to section 1.4.1, the only difference here is that the return variable is already defined in advance. In this case, you can understand that defer and return operations are effectively atomic, so the result of this example is 2.

1.4.3 Special Case

If the deferred function has any return values, they are discarded when the function completes

1
2
3
4
5
6
7
8
9
10
func deferReturnFn() int {
res := 1
defer func() int {
res++
return res
}()
return res
}
// output
1

At this point, the anonymous function’s result does not have a receiver, so it is discarded reasonably.

1.5 Notes

In addition to the notes mentioned earlier:

  • Stack-like invocation (LIFO - Last In, First Out)
  • Execution Order and Key Considerations
  • Return value handling

We should also pay attention to the placement of defer to avoid situations where resources are not released when the function exits.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// good
func readFile(filename string) {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
defer file.Close()
// do something
}

// bad
func readFile(filename string) {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
// do something
defer file.Close()
}

1.6 Philosophical Thoughts

Thinking about the philosophy of defer design from the perspective of resource cleaning, examples come from: 11 | 程序中的错误处理:错误返回码和异常捕捉-左耳听风-极客时间[15]

We all know that when a program encounters an error, it is necessary to clean up any resources that have already been allocated. In traditional approaches, each error requires cleaning up the resources allocated up to that point, which can lead to error-handling patterns like the “goto fail” style.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <stdlib.h>

#define FREE(p) if(p) { \
free(p); \
p = NULL; \
}

void ReportError(int errorCode) {
// Error handling code here
printf("Error: %d\n", errorCode);
}

int main() {
char *fname = NULL, *lname = NULL, *mname = NULL;

fname = (char*) calloc(20, sizeof(char));
if (fname == NULL) {
goto fail;
}

lname = (char*) calloc(20, sizeof(char));
if (lname == NULL) {
goto fail;
}

mname = (char*) calloc(20, sizeof(char));
if (mname == NULL) {
goto fail;
}

// Normal processing would happen here
// …

fail:
FREE(fname);
FREE(lname);
FREE(mname);
ReportError(1); // Example error code

return 0;
}

While this approach can work, it has potential issues. The most significant problem is that you cannot have return statements in the middle of the code because you need to clean up resources. Maintaining such code requires extra caution, as oversight can lead to resource leaks.

In contrast, C++’s RAII (Resource Acquisition Is Initialization) mechanism leverages object-oriented features to handle this issue more elegantly. RAII uses C++ classes to allocate resources in the constructor and release them in the destructor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <mutex>

// First, declare an RAII class. Pay attention to the constructor and destructor.
class LockGuard {
public:
LockGuard(std::mutex &m) : _m(m) {
m.lock(); // Lock the mutex when the LockGuard is constructed
}

~LockGuard() {
_m.unlock(); // Unlock the mutex when the LockGuard is destroyed
}
private:
std::mutex& _m;
};

// Now, let's see how to use it
void good() {
LockGuard lg(m); // RAII class: mutex is locked upon construction
f(); // If f() throws an exception, the mutex will be released
if (!everything_ok()) return; // Early return, mutex will be released when LockGuard is destroyed
} // If good() returns normally, the mutex is released

In Go, there is no class keyword. Instead, it achieves similar functionality using the defer keyword, which is clearly a better approach than the “goto fail” pattern and reduces the cognitive burden on programmers.

Go’s design philosophy is clearly influenced by the experiences of others, from the perspective of resource cleanup. We can see through the design philosophy behind defer, understanding Go’s design philosophy. I have seen many people writing Go code with the habits of other languages, which is exactly what shows they do not understand Go’s design philosophy, so they will write Go code that is awkward and hard to read.

1.7 Summary

This article starts from the official documentation of Go’s defer and explores its usage through examples, introducing the following content:

  1. Triggering Time
  2. Execution Order and Key Considerations
  3. Return Value Handling
  4. Notes

Finally, it introduces thoughts on the design of defer, which is a valuable keyword. Understanding and mastering it will help us write elegant Go code.

Of course, this article starts with observing the phenomena. Now, there are two key questions to address:

  1. How is the performance of defer? What optimizations has the Go team implemented?
  2. How is the design philosophy of defer implemented at the source code level?

Answering these two questions is indeed challenging, but this also means there is more to gain. I hope that in the future, I will have the time and energy to delve into these mysteries.

1.8 Reference

[1] return statement: https://go.dev/ref/spec#Return_statements
[2] function body: https://go.dev/ref/spec#Function_declarations
[3] panicking: https://go.dev/ref/spec#Handling_panics
[4]: https://go.dev/ref/spec#Defer_statements
[8] expression statements: https://go.dev/ref/spec#Expression_statements
[9] evaluated as usual: https://go.dev/ref/spec#Calls
[15] 11 | 程序中的错误处理:错误返回码和异常捕捉-左耳听风-极客时间: https://time.geekbang.org/column/article/675


More Series Articles about You Should Know In Golang:

https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a

And I’m Wesley, delighted to share knowledge from the world of programming. 

Don’t forget to follow me for more informative content, or feel free to share this with others who may also find it beneficial. it would be a great help to me.

Give me some free applauds, highlights, or replies, and I’ll pay attention to those reactions, which will determine whether or not I continue to post this type of article.

See you in the next article. 👋

中文文章: https://programmerscareer.com/zh-cn/golang-defer/
Author: Wesley Wei – Twitter Wesley Wei – Medium
Note: Originally written at https://programmerscareer.com/golang-defer/ at 2024-09-08 21:24.
Copyright: BY-NC-ND 3.0

Select Notions You Should Know in Golang [Pinned🔝] Sticky Note

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×