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’:
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”’.
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:
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:
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:
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!