How to use defer statement in golang
Defer statement is pretty simple to understand. But it has it’s gotchas. Mostly we learn from our mistakes, in this guide, you’ll learn from my mistakes. Lets get started.
First Thing: What is defer 🤔 ?
A defer statement defers the execution of a function until the surrounding function returns.
The deferred call’s arguments are evaluated immediately, but the function call is not executed until the surrounding function returns.
package main
import "fmt"
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
Output
hello
world
Defer is simply a convenience that lets you defer/push cleanup operations for later execution.
Consider this example of locking and unlocking a
Defer helps you keep resource allocation and cleanup together. Leaving less space for unintentional bugs. Other languages too have their own ways of doing this, but in go it’s called defer
.
How defer works ?
1. Defer uses stack order evaluation 📚
See below example with multiple defers in same function.
https://go.dev/play/p/6ixm52xVWDE
// Predict the order
package main
import "fmt"
func main() {
defer fmt.Println("1") // Push to stack
defer fmt.Println("2") // Push to stack
defer fmt.Println("3") // Push to stack
}
Output
3
2
1
Since defer uses stack, all Println
calls get pushed to stack.
__________________
| fmt.Println("3") | // Top of stack
|------------------|
| fmt.Println("2") |
|------------------|
| fmt.Println("1") | // Bottom
------------------
Once the main() or enclosing function completes, each item is popped of stack and executed. Hence the output in reverse order of defer statements.
2. Defer statements in a loop ➰
Predict output of below code.
https://go.dev/play/p/gqQsX0TYPh8
package main
import (
"fmt"
)
func main() {
for i := 0; i < 5; i += 1 {
defer fmt.Println("loop")
}
fmt.Println("main")
}
.
.
.
.
.
.
Output
main
loop
loop
loop
loop
loop
Quite expected right !! Since defer pushes function execution to function call stack, this means all the loop
’s get printed after enclosing function ends.
3. Arguments to deferred function are evaluated immediately 🐱🏍
package main
import (
"fmt"
"time"
)
func main() {
defer fmt.Println("Defer", time.Now()) // What does this print ?
fmt.Println("Before", time.Now())
time.Sleep(time.Second)
fmt.Println("After", time.Now())
}
Output
Before 2009-11-10 23:00:00 +0000 UTC m=+0.000000001
After 2009-11-10 23:00:01 +0000 UTC m=+1.000000001
Defer 2009-11-10 23:00:00 +0000 UTC m=+0.000000001
Notice that even though defered statement will get executed after fmt.Println(“After”, time.Now())
, still it prints lesser value of time.
Solution: Wrap in a function. This is a common pattern you should be aware of to avoid such issues.
package main
import (
"fmt"
"time"
)
func main() {
defer func() {
fmt.Println("Defer", time.Now()) // What does this print now ?
} () // Remember to call ()
fmt.Println("Before", time.Now())
time.Sleep(time.Second)
fmt.Println("After", time.Now())
}
Output
Before 2009-11-10 23:00:00 +0000 UTC m=+0.000000001
After 2009-11-10 23:00:01 +0000 UTC m=+1.000000001
Defer 2009-11-10 23:00:01 +0000 UTC m=+1.000000001
Also guess this one yourself
https://go.dev/play/p/SXF6kV7jJpM
package main
import (
"fmt"
)
func main() {
i := 0
defer func(val int) {
fmt.Println("Defer", val) // What does this print
}(i)
i += 1
fmt.Println("i = ", i)
}
4. Variable/Local state capture
What does example print 🙈
https://go.dev/play/p/pyPfLyQ7UMw
package main
import (
"fmt"
)
func main() {
for i := 0; i < 5; i += 1 {
defer fmt.Println("Count", i) // (0 1 2 3 4) or (4 3 2 1 0) or (4 4 4 4 4) ?
}
fmt.Println("main")
}
.
.
.
.
Output
main
Count 4
Count 3
Count 2
Count 1
Count 0
No tricks here 🎃. Simple stuff. Since defer evaluates args when you write defer, each call in the loop captures the current value and pushes a copy onto stack.
Well… what do you think this prints
https://go.dev/play/p/lRboy_DdDu_9
package main
import (
"fmt"
)
func main() {
count := 0
for i := 0; i < 5; i += 1 {
count += 1
defer func() { // Notice the enclosing function here
fmt.Println("Count", count)
}()
}
fmt.Println("main")
}
Maybe you didn’t expect this, but it’s true
main
Count 5
Count 5
Count 5
Count 5
Count 5
If this kind of behavior is a problem for your use case. Think how to fix this. Hint, pass count as argument 😉.
Lets do some nested calls.
What does this print ?
https://go.dev/play/p/TV1fyqNCuBX
package main
import (
"fmt"
)
func dummy(msg string) {
defer fmt.Println("dummy defer", msg)
fmt.Println("dummy", msg)
}
func main() {
dummy("1")
defer dummy("2")
fmt.Println("main")
}
Output
dummy 1
dummy defer 1
main
dummy 2
dummy defer 2
Defer usage patterns
1. Interleave defer correctly when calling functions that return an error
Problem:
Look at below code which doesn’t use defer.
package main
import (
"fmt"
"os"
)
func doSomething(_ *os.File) {
}
func main() {
file, err := os.Create("temp.txt") // 1. Function Call
if err != nil { // 2. Handle error
fmt.Println("Error creating file", err)
return
}
doSomething(file)
file.Close() // 3. Free up resource
}
Do you see any problem here ? 👀
The issue is if doSomething
crashes for some reson, file.Close
never runs. Consider this running as a http handler. Each call where doSomething
crashes, leaks a resource 💣.
The Fix ⚒
We should defer here to close the file after opening it
package main
import (
"fmt"
"os"
)
func doSomething(_ *os.File) {
}
func main() {
file, err := os.Create("temp.txt") // allocate
defer file.Close() // free
if err != nil { // error check
fmt.Println("Error creating file", err)
return
}
doSomething(file)
}
Wait 🛑🤚🏻🛑.
Is this really correct? NO
What happens if os.Create
fails ? file
will likely be nil
and file.Close()
will crash your code.
So the right pattern to defer cleanup actions is immediately after error check.
The Real Fix 👷🏻♂️
package main
import (
"fmt"
"os"
)
func doSomething(_ *os.File) {
}
func main() {
file, err := os.Create("temp.txt") // 1. Function Call
if err != nil { // 2. Handle error
fmt.Println("Error creating file", err)
return
}
defer file.Close() // 3. Defer cleanup ops
doSomething(file)
}
So the correct order always looks like
- Create resource
- Do error check and return
- defer resource cleanup
Before we move ahead, try to run the first example with some invalid file name to try to crash it and see what happens… well… it doesn’t crash 😆.
No 🎇🎆🔥.
No segmentation fault.
Here I try passing an empty file name. https://go.dev/play/p/Z5Ef1Bh69g_E
package main
import (
"fmt"
"os"
)
func doSomething(_ *os.File) {
}
func main() {
file, err := os.Create("") // pass empty file here
defer file.Close()
if err != nil {
fmt.Println("Error creating file", err)
return
}
doSomething(file)
}
It works perfectly fine and prints this:
Error creating file open : no such file or directory
If you are thinking why, check out the implementation of Close
: https://cs.opensource.google/go/go/+/refs/tags/go1.20.3:src/os/file_posix.go;l=21
Notice the nil check there. But don’t expect things to always be like this and always use defer correctly.
Perfect !! 👌🏻 Let’s move on.
2. Working with WaitGroups
This is really interesting. Read line by line carefully, then try running this code yourself.
https://go.dev/play/p/lbxLH_4tK5_h
package main
import (
"fmt"
"sync"
)
var i = 0
func incr() {
i += 1
}
// +100,000
func incrLoop() {
for i := 1; i <= 100000; i += 1 {
incr()
}
}
func main() {
var wg sync.WaitGroup
// +100,000
wg.Add(1) // This needs to be outside.
go func() {
defer wg.Done()
incrLoop()
}()
// another +100,000 in parallel
wg.Add(1)
go func() {
defer wg.Done()
incrLoop()
}()
wg.Wait()
fmt.Println("i=", i) // if everything works, i should be 200 000
}
What did you get ?
i= 200000
really ?? 🤷🏻 not possible except if you are really lucky 🤞🏻
Run ️a couple more times till you start seeing things like this
i= 141663
or
i= 156991
or anything which isn’t 200,000.
So the problem isn’t with waitgroup, we are using it perfectly fine. Try removing wait group and you’ll start seeing i= 0
.
Let’s fix the issue in the next one.
Read more about waitgroup patterns and how to use it properly here.
3. Working with mutex
So the problem with previous code was… incr()
gets called from two functions/goroutines in parallel. This is a classic problem with a classic solution.
https://go.dev/play/p/SKbmJW1nuRy
package main
import (
"fmt"
"sync"
)
var i = 0
var mu sync.Mutex // We added this
func incr() {
mu.Lock() // + this
defer mu.Unlock() // + this
i += 1
}
// +100,000
func incrLoop() {
for i := 1; i <= 100000; i += 1 {
incr()
}
}
func main() {
var wg sync.WaitGroup
// +100,000
wg.Add(1)
go func() {
defer wg.Done()
incrLoop()
}()
// another +100,000 in parallel
wg.Add(1)
go func() {
defer wg.Done()
incrLoop()
}()
wg.Wait()
fmt.Println("i=", i)
}
This correctly prints
i= 200000
Problem solved 🙌🏻
There can be better ways of solving this, but this works as a good enough example.
More examples coming soon.
Find me on Linkedin 👋🏻.