Created 2024/11/02 at 11:28AM
Last Modified 2025/01/09 at 09:04PM
Today we are going to talk about the defer statement in golang. There are various ways people write defer statements in their code, based on what behavior they want.
From A Tour of Go
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.
// main.go
package main
import (
"errors"
"fmt"
"math/rand"
)
type Resource struct {
Buffer []string
}
func Contains(source []string, target string) bool {
for _, v := range source {
if v == target {
return true
}
}
return false
}
func (r *Resource) Cleanup(itemsToDelete []string) error {
if rand.Intn(30) < 10 {
return errors.New("cleanup panic")
}
var newBuffer []string
for _, v := range r.Buffer {
if Contains(itemsToDelete, v) {
continue
}
newBuffer = append(newBuffer, v)
}
r.Buffer = nil
r.Buffer = newBuffer
return nil
}
func GetItemsFromService() ([]string, error) {
return []string{"hello", "apple"}, nil
}
func Task1(r *Resource, items []string) ([]string, error) {
defer r.Cleanup(items)
// someone forgot that items are being used for cleanup and modified it
serviceItems, err := GetItemsFromService()
if err != nil {
return nil, err
}
items = append(items, serviceItems...)
return items, nil
}
func Task2(r *Resource, items []string) ([]string, error) {
defer func() {
r.Cleanup(items)
}()
// someone forgot that items are being used for cleanup and modified it
serviceItems, err := GetItemsFromService()
if err != nil {
return nil, err
}
items = append(items, serviceItems...)
return items, nil
}
func Task3(r *Resource, items []string) ([]string, error) {
defer func(items []string) {
r.Cleanup(items)
}(items)
// someone forgot that items are being used for cleanup and modified it
serviceItems, err := GetItemsFromService()
if err != nil {
return nil, err
}
items = append(items, serviceItems...)
return items, nil
}
func main() {
resources := []*Resource{
{Buffer: []string{"hello", "pineapple", "yellow"}},
{Buffer: []string{"hello", "pineapple", "yellow"}},
{Buffer: []string{"hello", "pineapple", "yellow"}},
}
fmt.Println(Task1(resources[0], []string{"yellow"}))
fmt.Println(resources[0].Buffer)
fmt.Println(Task2(resources[1], []string{"yellow"}))
fmt.Println(resources[1].Buffer)
fmt.Println(Task3(resources[2], []string{"yellow"}))
fmt.Println(resources[2].Buffer)
}
So, if everything goes fine and there's no error in cleanup, we get following output
[yellow hello apple] <nil>
[hello pineapple]
[yellow hello apple] <nil>
[pineapple]
[yellow hello apple] <nil>
[hello pineapple]
So the way defer
statement is being used in Task1
, should never be used, and is something people often miss. Why? Because r.Cleanup()
can result into an error (lets say you are closing a database connection, but it failed due to network connectivity or whatever other reason), your program will run ignoring the error from defer, and you will never know that there was some issue in resource cleanup.
A better way is to always defer such things with a closure, like we are doing in Task2
and Task3
func Foo() {
defer func() {
Cleanup()
} ()
}
Now if you noticed, the Buffer
is different in Resource
objects after Task2
and Task3
, why?
Well, defer
in Task3
captures the value of items
immediately in the closure, while defer
in Task2
reflects upon the final value of items
. In our case, we would want the behavior in Task3
while in some cases, you might prefer the behavior in Taskj2
depending on your use case.
The key point here is that enclosing defer
call with a closure gives you ability to add additional logging and recovery (if possible) and alerting in your resource cleanups. So you would do something like
func Foo() {
defer func() {
err := Cleanup()
if err != nil {
sendAlert(fmt.Sprintf("cleanup failed due to %w", err))
}
} ()
}