How to use defer statement in golang

leangaurav
6 min readApr 21, 2023

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.

Source: https://go.dev/tour/flowcontrol/12

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

  1. Create resource
  2. Do error check and return
  3. 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 👋🏻.

--

--

leangaurav

Engineer | Trainer | writes about Practical Software Engineering | Find me on linkedin.com/in/leangaurav | Discuss anything topmate.io/leangaurav