Video:

Defer in Go

June 24, 2021

Course Instructor: Elliot Forbes

Hey Gophers! My name is Elliot and I'm the creator of TutorialEdge and I've been working with Go systems for roughly 5 years now.

We’ve had a small glimpse as to what deferred functions are in Go and how they can be used in the previous video. In this video, we are going to be diving deeper and covering the topic more in-depth!

Deferred Functions

Deferred functions are something that are used a lot within Go when it comes to things like setting up connections to say, databases, or brokers. Typically we end up using a deferred function to ensure that connections are closed after we are done with them so we don’t have stale connections to the database.

Defining our Own Deferred Functions

Let’s have a look at how we can define our own deferred functions in a small example. We’ll use the defer keyword in front of what is called an anonymous function. These anonymous functions act just like normal functions except they don’t have a name, however they can accept inputs and return outputs just the same.

In this example, we’ll just make our anonymous function print out ‘I will be executed at the end of the main function’:

main.go
package main

import "fmt"

func main() {
	defer func() {
		fmt.Println("I will be executed last")
	}()

	defer func() {
		fmt.Println("Last in - but first out")
	}()

	fmt.Println("defer in go")
}

So, when we run this, we expect that our main function will run through everything below the deferred functions first and when it reaches the end of the body of the function it will then attempt to execute each of the deferred functions in a LIFO (last-in, first-out) order.

I like to think of deferred functions as mental todos within my application that say things like ‘hey, I opened up a connection, let’s immediately add a todo that says “close this connection when I’m finished”’.

$ go run main.go
defer in go
Last in - but first out
I will be executed last

Modifying Return Values

In the past video, we looked at how we could catch a panic and return an error from within a deferred function. I glossed over this as the point of that video wasn’t to cover the deferred function, so let’s have a look at what we’ve done in more detail:

func MyAwesomeFunction() (err error) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("recovered from a small panic")
			err = errors.New("i panic easily panicked")
		}
		fmt.Println("2")
	}()
	IPanicEasily()
	return nil
}

Here, we have defined a defer function that calls the built-in recover function that will allow us to recover from any panics thrown during the execution of this function.

One interesting thing to note here though is that we’ve used a named return value err which we’ve then updated the value of within our deferred function.

Let’s see this again in another example:

package main

import "fmt"

func ReturnValues() (x int) {
  defer func() {
    x = 10
  }()
  x = 5
  return
}

func main() {
  x := ReturnValues
  fmt.Println(x)
}

Let’s try and run this now, we should see that the value of x is updated after we’ve set x = 5 within the ReturnValues function:

$ go run main.go
defer in go
10

Deferred Methods

Deferred functions are not just limited to function calls, we also have the ability to defer calls to methods on a struct. Let’s see an example of this now:

package main

import "fmt"

type Engineer struct {
  Name string
}

func (e *Engineer) UpdateName(newName string) {
  e.Name = newName
}

func doStuff(e *Engineer) {
  defer e.UpdateName("Elliot Forbes")
  fmt.Println("Executing other stuff...")
}

func main() {
  elliot := &Engineer{
    Name: "Elliot",
  }

  fmt.Printf("%+v\n", elliot)
  doStuff(elliot)
  fmt.Printf("%+v\n", elliot)
}

Let’s see this in action now:

$ go run main.go
defer in go
&{Name:Elliot}
doing other exciting stuff
&{Name:Elliot Forbes}

As you can see, the method has been executed successfully as a deferred function and it has updated the name of the Engineer variable to my full name!

Advantages of Defer

It’s worth highlighting at this point some of the main advantages of deferred functions. The biggest of which is the ability to group common code together in semantic blocks which makes it easier to read/maintain/debug.

Let’s take for example a call to a HTTP REST API using the http package:

func Crawl(url string) {
  resp, err := http.Get(url)
  if err != nil {
    fmt.Println(err)
  }
  defer resp.Body.Close()

  // do other stuff with the response
}

In this example, we’ve grouped the code to perform a http request as well as clean up and close our response body together. The alternative to this would be to process the response body and then remember to close the body at the end of the function but this means that we’ve effectively scattered the code to make this request across the entire body of the function.

Doing it this way makes it easier to remember. When performing these HTTP requests, you quickly get into the habit of remembering to close if you always write your http requests using this same method.

Gotchas

One key gotcha to worry about is the fact that deferred function arguments are evaluated immediately with only the body of the function being deferred to execute at the end of the function.

What does this mean?

Well, let’s say you are passing in values to a deferred function:

package main

import (
	"fmt"
)

type Engineer struct {
	Name string
}

func (e *Engineer) UpdateName(name string) {
	e.Name = name
}

func doStuff(e *Engineer) {
	name := "Elliot Forbes"
	defer e.UpdateName(name)
	fmt.Println("doing other exciting stuff")

	name = "Elliot M Forbes"
}

func main() {
	fmt.Println("defer in go")
	elliot := &Engineer{
		Name: "Elliot",
	}

	fmt.Printf("%+v\n", elliot)
	doStuff(elliot)
	fmt.Printf("%+v\n", elliot)
}

Let’s give this a shot now and see what happens:

$ go run main.go
defer in go
&{Name:Elliot}
doing other exciting stuff
&{Name:Elliot Forbes}

As you can see from the output, name has been immediately evaluated and then executed at the end of our function. Regardless of the fact we’ve updated the name variable to a new value, the deferred function has been executed with the value of name at the point at which we called defer.

Conclusion

Awesome, so let’s recap what we’ve covered in this video! We’ve looked at how you can use the defer keyword in front of both anonymous and named functions in Go in order to add ‘todos’ to our function that will be executed after the function has finished its execution.

We’ve also looked at how we can use named return values in conjunction with these deferred functions in order to return errors should anything unexpected happen during their execution!