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
Hello, here is Wesley, Today’s article is about defer
in Go. Without further ado, let’s get started.💪
1.1 Use Cases
- Resource Cleanup: For example, closing files, database links, or network links.
1 | func readFile(filename string) { |
- Unlocking Resources: For example, ensuring locks are unlocked or executing wg.Done in concurrent programming.
1 | var mu sync.Mutex |
- 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 | package main |
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]
- executed a return statement[5]
- reached the end of its **function body[6]
- corresponding goroutine is panicking[7]
1 | func deferFn1() { |
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
- 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].
- 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 | func main() { |
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 | func main() { |
We can approach these two examples from two different perspectives:
- 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 ofstartedAt
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 ofstartedAt
is “captured” when the deferred function runs, not when it’s declared.
- 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
- 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.
- 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
- 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 | func deferReturnFn() int { |
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 | func deferReturnFn() int { |
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 | func deferReturnFn() (res int) { |
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 | func deferReturnFn() int { |
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 | // good |
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 | #include <stdio.h> |
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 | #include <mutex> |
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:
- Triggering Time
- Execution Order and Key Considerations
- Return Value Handling
- 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:
- How is the performance of
defer
? What optimizations has the Go team implemented? - 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
Comments