In Valid Logic

Endlessly expanding technology

Golang Tidbit: Defer

A while ago, I did a post on Golang Oddities. I only made one post in what I intended to make a series of, but at any rate, I’d realized “oddity” wasn’t really the right word. I was intending it more as an interesting bit to be aware of than knocking the language.

One interesting one to be aware of is how defer works within the language. A article on how defer, panic, and recover work briefly mentions something:

A deferred function’s arguments are evaluated when the defer statement is evaluated.

They offer up a simple code snippet to highlight the fact:

// try at http://play.golang.org/p/PYaIIMAqHj
package main

import "fmt"

func main() {
	i := 0
	defer fmt.Println(i)
	i++
	return
}

When run, this will print out 0 even though i++ is executed before the call to print. The way defer works is it does everything it needs to do to get ready to execute an expression, except it delays the actual execution. So anything that is an argument to the call is evaluated at the point in the method the defer is at, then executes the actual expression after the return.

The behavior looks innocuous, but can manifest itself in some auspicious ways. For instance:

// try at http://play.golang.org/p/DaJvfDRJ3Y
package main

import "fmt"

type printer struct {
	message string
}

func (p *printer) SetMessage(msg string) {
	p.message = msg
}

func (p *printer) Print() string {
	return p.message
}

func main() {
	p := printer{}
	p.SetMessage("Starting")
	defer fmt.Println(p.Print())
	p.SetMessage("Done")
}

This seems normal enough, but now instead of passing in a variable, the arugment is from a function call on a struct. The same behavior will result. It prints "Starting" instead of "Done".

However, you also have to be aware of what is being passed into anything being evaluated. In the above examples, simple non-pointer types were being passed in. So essentially a copy of the variable was being created and passed to the call that was being deferred.

On the other hand, take the following example:

// try at http://play.golang.org/p/rIFL-dPBrW
package main

import "fmt"

func printStr(str *string) {
	fmt.Println(*str)
}

func main() {
	s := "Starting"
	defer printStr(&s)
	s = "Done"
}

In this example, a pointer to a string is being passed to the printStr function. Because a pointer is being passed in, assignments that happen after the defer statement are carried over.

So how can this be worked around? The simple way is through an inline function. Instead of calling what you want to call directly, create an inline function around it. Evaluating the function at the time is simple, since there are usually no parameters. But when it is run, it is still in scope of the variables within the main function.

// try at http://play.golang.org/p/BXHnCikUQj
package main

import "fmt"

func main() {
	i := 0
	defer func() { fmt.Println(i) }()
	i++
	return
}

It is important to note the () at the end. You can’t defer a function type, you need to defer an expression. So the inline function needs to actually be called. The same is true with the go keyword to execute a statement in another goroutine.

Despite some of the gotchas with how defer works, it is definitely one of my favorite parts of Go. Instead of needing to scatter around cleanup code in a function, it allows you put cleanup right after dirtying. Say you have to do 5 different things which involve opening files, sockets, etc. Instead of mucking with cleaning up if the function fails at step 3 and cleaning up #1 and #2, you simply defer the cleanup after each step.

For example, take the following snippet. This is more psuedo code, not any of our actual code, but in it, we can use a local variable to track if we succeded, and can check it on the way out to see if everything was successful.

func createUserAndDatabase(name string) error {
	// understand if we succeeded
	success := false

	// connect
	db, err := connectToDB()
	if err != nil {
		return err
	}
	defer db.Close()

	// create user
	user, err := db.CreateUser(name)
	if err != nil {
		return err
	}
	defer func() {
		if !success {
			db.DeleteUser(user)
		}
	}()

	// create database
	newdb, err := db.CreateDatabase(name)
	if err != nil {
		return err
	}

	// we're done, mark success
	success = true
    return nil
}

Another way it could be done is with a named return variable. In the function definition, give the error object a name and it can be accessed in the deferred call. If no error is being returned, then function succeeded.

Overall, defer is excellent to work with and hope you find it awesome too.

Monday, July 01, 2013

 
blog comments powered by Disqus