Common traps while using defer in go
The defer statement in go is really handy in improving code readability. However, in some cases its behavior is confusing and not immediately obvious. Even after writing go for over 2 years, there are times when a defer in the wild leaves me scratching my head. My goal is to compile a list of behaviors which have stumped me in the past, mainly as a note to myself.
Defer scopes to a function, not a block
A variable exists only within the scope of a code block. However, a defer statement within a block is only executed when the enclosing function returns. I’m not sure what the rationale for this is, but it can catch you off guard if you’re, say, allocating resources in a loop but defer the deallocation.
func do(files []string) error {
for _, file := range files {
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close() // This is wrong!!
// use f
}
}
Chaining methods
If you chain methods in a defer statement, everything except the last function will be evaluated at call time. defer expects a function as the “argument”.
type logger struct {}
func (l *logger) Print(s string) {
fmt.Printf("Log: %v\n", s)
}type foo struct {
l *logger
}func (f *foo) Logger() *logger {
fmt.Println("Logger()")
return f.l
}func do(f *foo) {
defer f.Logger().Print("done")
fmt.Println("do")
}
func main() {
f := &foo{
l: &logger{},
}
do(f)
}
Prints —
Logger()
do
Log: done
The Logger()
function is called before any of the work in do()
is executed.
Function arguments
Okay, but what if the last method in the chain takes an argument? Surely, if it is executed after the enclosing function returns, any changes made to the variables will be captured.
type logger struct {}
func (l *logger) Print(err error) {
fmt.Printf("Log: %v\n", err)
}type foo struct {
l *logger
}func (f *foo) Logger() *logger {
fmt.Println("Logger()")
return f.l
}func do(f *foo) (err error) {
defer f.Logger().Print(err)
fmt.Println("do")
return fmt.Errorf("ERROR")
}
func main() {
f := &foo{
l: &logger{},
}
do(f)
}
Guess what this prints?
Logger()
do
Log: <nil>
The value of err
is captured at call time. Any changes made to this variable are not captured by the defer statement because they don’t point to the same value.
Calling methods on non-pointer types
We saw how chained methods behave in a defer statement. Exploring this further, if the called method is not defined on a pointer receiver type, calling it in a defer will actually make a copy of the instance.
type metrics struct {
success bool
latency time.Duration
}func (m metrics) Log() {
fmt.Printf("Success: %v, Latency: %v\n", m.success, m.latency)
}func foo() {
var m metrics
defer m.Log() start := time.Now()
// Do something
time.Sleep(2*time.Second)
m.success = true
m.latency = time.Now().Sub(start)
}
This prints —
Success: false, Latency: 0s
m
is copied when defer is called. m.Foo()
is basically shorthand for Foo(m)
Conclusion
If you’ve spent enough time writing go, these might not feel like “traps”. But for someone new to the language, there are definitely a lot of places where the defer statement does not satisfy the principle of least astonishment. There are a bunch of other places that go into more detail about some other common mistakes while writing go. Do check them out.