1 Introduction to the defer
feature in Golang
In Go language, the defer
statement delays the execution of the function call following it until the function containing the defer
statement is about to finish execution. You can think of it as the finally
block in other programming languages, but the usage of defer
is more flexible and unique.
The benefit of using defer
is that it can be used to perform cleanup tasks, such as closing files, unlocking mutexes, or simply recording the exit time of a function. This can make the program more robust and reduce the amount of programming work in exception handling. In Go's design philosophy, the use of defer
is recommended because it helps keep the code concise and readable when handling errors, resource cleanup, and other subsequent operations.
2 Working principle of defer
2.1 Basic working principle
The basic working principle of defer
is to use a stack (last in, first out principle) to store each deferred function to be executed. When a defer
statement appears, Go language does not immediately execute the function following the statement. Instead, it pushes it into a dedicated stack. Only when the outer function is about to return, these deferred functions will be executed in the order of the stack, with the function in the last declared defer
statement being executed first.
Additionally, it is worth noting that the parameters in the functions following the defer
statement are calculated and fixed at the moment when the defer
is declared, rather than at the actual execution.
func example() {
defer fmt.Println("world") // deferred
fmt.Println("hello")
}
func main() {
example()
}
The above code will output:
hello
world
world
is printed before the example
function exits, even though it appears before hello
in the code.
2.2 Execution order of multiple defer
statements
When a function has multiple defer
statements, they will be executed in a last in, first out order. This is often very important for understanding complex cleanup logic. The following example demonstrates the execution order of multiple defer
statements:
func multipleDefers() {
defer fmt.Println("First defer")
defer fmt.Println("Second defer")
defer fmt.Println("Third defer")
fmt.Println("Function body")
}
func main() {
multipleDefers()
}
The output of this code will be:
Function body
Third defer
Second defer
First defer
Since defer
follows the last in, first out principle, even though "First defer" is the first deferred, it will be executed last.
3 Applications of defer
in different scenarios
3.1 Resource release
In Go language, the defer
statement is commonly used to handle resource release logic, such as file operations and database connections. defer
ensures that after the function execution, the corresponding resources will be properly released regardless of the reason for leaving the function.
Example of file operation:
func ReadFile(filename string) {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
// Use defer to ensure the file is closed
defer file.Close()
// Perform file reading operations...
}
In this example, once os.Open
successfully opens the file, the subsequent defer file.Close()
statement ensures that the file resource will be correctly closed and the file handle resource will be released when the function ends.
Database Connection Example:
func QueryDatabase(query string) {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
// Ensure the database connection is closed using defer
defer db.Close()
// Perform database query operations...
}
Similarly, defer db.Close()
ensures that the database connection will be closed when leaving the QueryDatabase
function, regardless of the reason (normal return or exception thrown).
3.2 Lock Operations in Concurrent Programming
In concurrent programming, using defer
to handle the release of mutex locks is a good practice. It ensures that the lock is correctly released after the critical section code execution, thereby avoiding deadlocks.
Mutex Lock Example:
var mutex sync.Mutex
func updateSharedResource() {
mutex.Lock()
// Use defer to ensure that the lock is released
defer mutex.Unlock()
// Perform modifications to the shared resource...
}
Regardless of whether the modification of the shared resource is successful or if a panic occurs in between, defer
will ensure that Unlock()
is called, allowing other goroutines waiting for the lock to acquire it.
Tip: Detailed explanations about mutex locks will be covered in the subsequent chapters. Understanding the application scenarios of defer is sufficient at this point.
3 Common Pitfalls and Considerations for defer
When using defer
, although the readability and maintainability of the code are greatly improved, there are also some pitfalls and considerations to keep in mind.
3.1 Deferred function parameters are evaluated immediately
func printValue(v int) {
fmt.Println("Value:", v)
}
func main() {
value := 1
defer printValue(value)
// Modifying the value of `value` will not affect the parameter already passed to defer
value = 2
}
// The output will be "Value: 1"
Despite the change in the value of value
after the defer
statement, the parameter passed to printValue
in the defer
is already evaluated and fixed, so the output will still be "Value: 1".
3.2 Be cautious when using defer inside loops
Using defer
inside a loop can lead to resources not being released before the loop ends, which may result in resource leaks or exhaustion.
3.3 Avoid "release after use" in concurrent programming
In concurrent programs, when using defer
to release resources, it is important to ensure that all goroutines will not attempt to access the resource after it has been released, to prevent race conditions.
4. Note the execution order of defer
statements
defer
statements follow the Last-In-First-Out (LIFO) principle, where the last declared defer
will be executed first.
Solutions and Best Practices:
- Always be aware that function parameters in
defer
statements are evaluated at the time of declaration. - When using
defer
inside a loop, consider using anonymous functions or explicitly calling resource release. - In a concurrent environment, ensure that all goroutines have finished their operations before using
defer
to release resources. - When writing functions containing multiple
defer
statements, carefully consider their execution order and logic.
Following these best practices can avoid most of the issues encountered when using defer
and result in writing more robust and maintainable Go code.