Understanding Defer In Golang

Understanding Defer In Golang

In this article, we will be exploring what is defer in Golang, its uses, and how to implement it to avoid bugs. The most common use case of defer is for cleaning up resources before a function exits.

What is defer?

  • Defer is used to ensure that a function call is performed later in a program’s execution before the function returns.
  • defer makes our code cleaner and less error-prone by keeping the calls to close the file/resource in proximity to the open call.
  • Cleaning up resources, such as open files, network connections, and database handlers.

Here is a simple example using the defer keyword.

package main

import "fmt"

func main() {
    defer fmt.Println("Defer executes before the function returns!")
    fmt.Println("Hello! In main function")
}

Output for the above code.

Hello! In main function
Defer executes before the function returns!

Understanding Defer with Example

Let’s take an example where you are accessing the file system i.e performing some operations on file like read/write. We need to load and read the contents of the file.

As mentioned before, we would want to ensure that resource cleanup is performed before we exit the method.

Let’s write a function that creates a file and writes a string to it.

package main

import (
  "fmt"
  "os"
)

func writeToFile(filename string) error {
  fmt.Println("Open the file")
  file, err := os.Open(filename)
  if err != nil {
      return err
  }

  fmt.Println("Write to file")
  _, err := f.WriteString("writes\n")
  if err != nil {
      return err
  }
  return nil
}

func main() {
  writeToFile("hello.txt")
}

The code above works as expected but there is a bug. Can you spot it? What if the f.WriteString() fails and returns an error. Our function returns error but the source file resource is not closed.

This can be solved with the help of defer. We add a defer on file.Close() to close source file even if the function returns an error.

package main

import (
    "fmt"
    "os"
)

func writeToFile(filename string) error {
    fmt.Println("Open the file")
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()

    fmt.Println("Write to file")
    _, err := f.WriteString("writes\n")
    if err != nil {
        return err
    }
    return nil
}

func main() {
    writeToFile("hello.txt")
}

Important Points to Remember

  • Defer executes in LIFO - Last in first out order. This can be verified using this example.

    package main
    
    import "fmt"
    
    func main() {
        defer fmt.Println("First")
        defer fmt.Println("Second")
        defer fmt.Println("Third")
    }
    

    Since all three print statements are with defer. Execution takes place in LIFO order and the following output is generated.

    Third
    Second
    First
    
  • Multiple defer statements can be used to release different resources used in a function. The point to remember is that the order of execution will be the opposite of what they are mentioned.

  • defer can be placed anywhere within the function since they are executed before the function returns.

    package main
    
    import "fmt"
    
    func main() {
        fmt.Println("Start")
        defer fmt.Println("Defer")
        fmt.Println("End")
    }
    

    Now let’s change the order for defer statement.

    package main
    
    import "fmt"
    
    func main() {
    defer fmt.Println("Defer")
        fmt.Println("Start")
        fmt.Println("End")
    }
    

    Both code return the following output.

    Start
    End
    Defer
    
  • Defining defer in correct function scope. Recently at work, I faced an issue due to this. Let’s try to explore this using a real world example.

    Real world example: Suppose you have a function in which you perform multiple tasks say validating input, some DB operations, publishing logs, etc.

    Now here you may want some operations to be asynchronous. You create a Goroutine and pass a context with a defined timeout. If you defer the cancel function of context without the anonymous function your synchronous code gets executed and the goroutine would cancel due to defer call after the original function has executed.

    Here publishKafka would fail due to defer cancelFunc() being in the wrong context.

    func somefunc() {
        // peform some action
        ...
        ctx := context.Background()
        ctx, cancelFunc := context.WithTimeout(ctx, 1 * time.Second)
        // This will cancel the goroutine once somefunc() is executed.
        defer cancelFunc()
    
        // publish logs to kafka asynchronously
        func() {
            go publishLogs(ctx)
        }
        // continue with other sync execution
        updateDB()
    }
    

    To prevent this you can add an anonymous function that allows the goroutine to execute separately and with defer defined in that function’s scope this would work as expected. An important takeaway is to place the defer under the right function scope.

    func somefunc() {
        // peform some action
        ...
    
        // publish logs to kafka asynchronously
        func() {
            ctx := context.Background()
            ctx, cancelFunc := context.WithTimeout(ctx, 1 * time.Second)
            go publishLogs(ctx)
            // This will wait for publishLogs to return
            // So this runs independent of somefunc() execution
            defer cancelFunc()
        }
        // continue with other sync execution
        updateDB()
    }
    
  • Deferred calls are executed even when the function panics.

    package main
    
    import "fmt"
    
    func main() {
        defer fmt.Println("Defer even works in panic")
        panic("Stop right now")
        fmt.Println("Still working")
    }
    
  • Recover and catching panic in a goroutine with defer. If a goroutine panics, recover stops the unwinding and returns the argument passed to panic else it simply returns nil.

    package main
    
    import (
        "fmt"
    )
    
    func main() {
        num := panicFun()
        fmt.Println("Returned value: ", num)
    }
    
    func panicFun() (num int) {
        defer func() {
            if err := recover(); err != nil {
                fmt.Println(err)
                num = -1
            }
        }()
        num = 1
        panic("let's panic")
        return num
    }
    
      // Output
      // let's panic
      // Returned value: -1
    

Resources

Books to learn Golang

Popular Go Articles


I hope you learned something new. Feel free to suggest improvements ✔️

I share regular updates and resources on Twitter. Let’s connect!

Keep exploring 🔎 Keep learning 🚀

Liked the content? Do support :)

Paypal - Mohit Khare
Buy me a coffee