Go defer Can Mess Up Your Intended Code Logic
Usefulness of defer
defer
is a Go feature that defers execution until after the function exits. It is not simply a way to move a statement from the current line to the last line of the enclosing function. defer
can become problematic when multiple defer statements are involved (making execution order tricky to determine) or when it is used to enforce a logical execution order.
Two Common Pitfalls When Using defer
Misusing defer
can lead to unintended consequences. A common mistake is invoking a function call without wrapping it in an anonymous function when capturing a dynamically changing value. For example:
package main
import (
"fmt"
)
func world(val string) {
fmt.Printf("%s from world", val)
}
func main() {
val := "hi"
defer world(val)
val = "hello"
fmt.Println("hello from main")
}
At the point of invoking defer world(val)
, the value of val
is captured as "hi"
. Later changes to val
do not affect this deferred function call, which can be undesirable.
One such undesirable scenario is passing an error object. If we declare var err error
and attempt to defer funcName(err)
or channelName <- err
, the parameter err
is immediately evaluated, but execution is delayed. This can result in sending an outdated error value.
To fix this, we can use an anonymous function:
package main
import (
"fmt"
)
func world(val string) {
fmt.Printf("%s from world", val)
}
func main() {
val := "hi"
defer func() {
world(val)
}()
ch := make(chan string, 1)
defer func() {
v := <-ch
fmt.Printf("%s from channel\n", v)
}()
defer func() {
ch <- val
}()
val = "hello"
fmt.Println("hello from main")
}
This produces:
hello from main
hello from channel
hello from world
Here, we deferred the channel send operation. If the channel is used to signal the completion of an entire operation, this ensures it triggers at the correct time.
Another example:
func lastOperation() {
fmt.Println("Doing something")
}
func main() {
ch := make(chan string, 1)
defer lastOperation()
ch <- "done"
}
The channel is notified before lastOperation()
executes, making the logic incorrect. The last operation should be done before notifying completion, not the other way around.
One more noteworthy example on Reddit highlights how defer
delays evaluation:
type A struct {
text string
}
func (a *A) Do() {
_ = a.text
}
func DoSomething() {
var a *A
defer a.Do()
// vs
// defer func() { a.Do() }()
a = &A{}
}
defer a.Do()
causes a runtime panic because a
is nil
at the time of defer evaluation. However, using defer func() { a.Do() }()
delays evaluation, allowing a
to be assigned a valid value before execution.
Contrast it with:
type A struct {}
func (a *A) Do() {}
func DoSomething() {
var a *A
defer a.Do()
a = &A{}
}
Here, a
is still a nil
pointer at the time of defer evaluation, but since Do()
does not dereference a
, the call is safe.
To summarize, if function parameter evaluation is irrelevant, using defer funcName()
is fine. Otherwise, wrap it in an anonymous function to delay evaluation.
The second common pitfall is to register defer
statements too late in the function. This can result in them never executing if the function exits early (e.g., due to error handling).
Best practices:
- Wrap the
defer
call in an anonymous function if necessary to prevent immediate parameter evaluation. - Place
defer
statements as early as possible (and logical) in the function to ensure they are registered before any early return logic.
More on Multiple defer
When multiple defer
statements are used, their execution follows a stack-based order—Last In, First Out (LIFO). Deferred executions occur in reverse order from their placement in the function. Understanding this order is critical in cases like:
- Ensuring consistent mutex unlocking sequences.
- Correctly signaling completion in operations that depend on ordered execution.
Consider a structure where A
is an operation and A.a
is a sub-operation. Without defer, the correct order would be:
- Send done to
A.a
's channel. - Send done to
A
's channel.
But, with a single defer:
- (defer) Send done to
A.a
's channel. - Send done to
A
's channel.
This could lead to incorrect order (A
is marked done before A.a
).
A similar issue arises when both are deferred incorrectly:
- (defer) Send done to
A.a
's channel. - (defer) Send done to
A
's channel.
Since defer
follows LIFO, A
is marked done before A.a
.
Correcting defer order:
- (defer) Send done to
A
's channel. - (defer) Send done to
A.a
's channel.
Now, A.a
completes before A
, ensuring the correct sequence.