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.

What is defer?

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))
        }
    } ()
}