Filling Go Structs in VS Code

If you program in Go, then you work with structs all the time. There’s a handy little tool in VS Code that you can use to quickly populate an empty struct with all its fields instead of writing them by hand. With your cursor over the name of the struct, bring up the context menu by doing one of the following:

  • Clicking on the light bulb to the left
  • Pressing Ctrl+. (Windows/Linux)
  • Pressing Cmd+. (Max)
This context menu lets you fill a struct’s fields.

Then, click on the “Fill” option or press ENTER to accept it, and the struct’s fields will be added along with default values for each according to their type:

The struct’s fields have been added with default values.

This little productivity tool is great, especially when you’re mapping data across structs in different parts of your application.

Working with Maps in Go

In Go, a map is a data structure allowing you to store pairs of keys and values, while using the key to look up more complex data.

A few examples of maps, and a comparison to arrays/slices.

In other languages, you’ll find similar data structures called dictionary, hashtable, hashmap or even object. the ability to associate keys and values and the ability to perform this lookup very quickly makes maps extremely useful for many different applications, such as those in the image above and the list below.

  • Storing/retrieving customer data based on a government-issued ID number.
  • Translating English text to Morse code
  • Actual dictionaries, mapping words to a description, or words in one language to the equivalent in another
  • A telephone directory, associating names with telephone numbers
  • Storing product prices based on their barcodes
  • Grouping properties for a specific object (this can also be done with structs in Go, but only when all the properties are well-defined in advance)

The careful reader will note that arrays and slices have a similar capability of associating an index (key) with a value. However, there are two important differences:

  • Arrays and slices can only take zero or positive integer keys. Maps can use a wider variety of data types as keys, with strings being a popular choice.
  • Map keys have no particular order. Although they can be integers (similar to arrays and slices), they can be negative or have gaps (such as the “square roots” example in the image above).

Initialising Maps

A map is a generic data type so you need to decide the data type of the keys and values. (Interestingly, although general-purpose support for generics was added to the language as recently as 2022, built-in data structures such as arrays, slices and maps have been generic all along.) For instance, you can declare a map of string to string this way:

domainToCountry := map[string]string{}

Alternatively, you can use the built-in make() function. There’s no real difference between the two approaches if you’re declaring an empty map.

domainToCountry := make(map[string]string)

The map data type takes the form map[key]value. So if you want to declare a map of string to float32 instead, you do:

nameToPrice := map[string]float32{}

(Note that the use of float32 to represent money values isn’t a great idea due to floating-point error. This is just an example.)

If you want to initialise a map with data from the get-go, you initialise it with the curly brackets and add literal data between them:

	domainToCountry := map[string]string{
		"es": "Spain",
		"it": "Italy",
	}

Note that the comma is required even after the last item.

Outputting a Map

If you want to display the contents of a map for debugging or other purposes, simply dropping it into a fmt.Println() does the trick. If you want to display it as part of a format string, use the %v placeholder for the map.

package main

import "fmt"

func main() {
	domainToCountry := map[string]string{
		"es": "Spain",
		"it": "Italy",
	}
	fmt.Println(domainToCountry)
	fmt.Printf("My map: %v", domainToCountry)
}
The output displaying the map using fmt.Println() and fmt.Printf() can be seen in the Debug Console near the bottom of the window.

Getting Data from a Map

Use indexing syntax to get a value from a map by its key:

	country1, exists1 := domainToCountry["es"]
	country2, exists2 := domainToCountry["cat"]

	fmt.Printf("es: %s, %t\n", country1, exists1) // outputs: "es: Spain, true"
	fmt.Printf("es: %s, %t\n", country2, exists2) // outputs: "es: , false"

Doing this returns 2 values: the corresponding value of the key, and whether the key exists in the map. It’s a safe operation, so if the key doesn’t exist, the value returned will be the default value of the type (e.g. 0 for ints, "" for strings, etc), and the second return value will come back as false.

The second return value is in fact optional; you can omit it entirely if you just want the value back. But, it’s useful to check whether the key exists in the map, as a default value can otherwise be confused with a legit value (e.g. 0 could mean that the key isn’t in the map, or it could really be a value in the map).

country1 := domainToCountry["es"]

The first return value is also optional, so if you only care to check whether the key exists in the map, you can replace it with an underscore:

_, exists1 := domainToCountry["es"]

The existence check can also be done inline within an if statement. This has the advantage of limiting the scope of the key/value variables to the scope of the if statement, limiting the potential for accidental and erroneous usage in longer functions:

	if country1, exists1 := domainToCountry["es"]; exists1 {
		fmt.Printf("The entry for %s exists. Let's do something with it!\n", country1)
	}

Inserting/Updating Data in a Map

After a map has been initialised, you can add key-value pairs to it using indexing syntax:

domainToCountry["be"] = "Belgium"

If the key wasn’t present in the map, it gets added. If it was, then the value gets overwritten.

	domainToCountry["be"] = "Belgium"
	domainToCountry["be"] = "Belgium2"
	fmt.Println(domainToCountry) // outputs "map[be:Belgium2 es:Spain it:Italy]"

Removing Data from a Map

Use the built-in delete() function to remove a key and its corresponding value from the map. This function is safe and will do nothing if the key is not in the map.

	delete(domainToCountry, "be") // removed
	delete(domainToCountry, "aaa") // wasn't there, so no-op

Length of a Map

Use the built-in len() function to check how many keys are present in the map.

	length := len(domainToCountry)
	fmt.Println(length) // outputs 2

Iterating over a Map

Use a for ... range loop to iterate over the keys and/or values of a map:

	for domain, country := range domainToCountry {
		fmt.Printf("Extension %s belongs to %s\n", domain, country)
	}

The output of the above snippet would be:

Extension es belongs to Spain
Extension it belongs to Italy

Both the key and value are optional. If you want just the key, simply omit the value:

	for domain := range domainToCountry {
		fmt.Println(domain)
	}

Whereas if you just want the value, replace the key with an underscore:

	for _, country := range domainToCountry {
		fmt.Println(country)
	}

You could also omit both, but that’s not usually very useful:

	for range domainToCountry {
		fmt.Println("I don't know why I'm iterating over a map if I don't use its data")
	}

It’s important to note that when iterating over a map, there’s no clearly-defined order as there is in arrays and slices. If you iterate over the same map multiple times, don’t expect to see the data come out in the same order each time.

Iterating over the same map multiple times produces differently ordered results.

Clearing a Map

To delete all items from a map, all you need to do is re-initialise it. The memory used by the old keys and values will be freed when the garbage collector kicks in.

package main

import "fmt"

func main() {
	domainToCountry := map[string]string{
		"es": "Spain",
		"it": "Italy",
	}

	domainToCountry = map[string]string{} // clear map

	fmt.Println(domainToCountry) // outputs "map[]"
}

A Map of Slices

Now that we’ve covered basic usage of maps, let’s consider a few more elaborate scenarios. For starters, how do we store multiple values for each key? For instance, we want to create a telephone directory (name to telephone number) and each person can have multiple numbers. For that, we can use a map of string to slice of string ([]string):

telephoneDirectory := map[string][]string{}

Note that, as I wrote in “From .NET to GoLang: Where Did Everything Go?“, the map syntax starts to be very confusing when you go beyond maps of simple types, due to overuse of square brackets. Note also that I’m opting to use strings to represent telephone numbers because the latter sometimes have length or characters that integer data types can’t handle.

When we add entries to our directory, we have to be careful to check whether a list of numbers already exists for that particular name. If it does, we add to it; otherwise we initialise a new one.

package main

import "fmt"

func addToDirectory(telephoneDirectory map[string][]string, name, telephoneNumber string) {
	if _, exists := telephoneDirectory[name]; !exists {
		telephoneDirectory[name] = []string{}
	}

	telephoneDirectory[name] = append(telephoneDirectory[name], telephoneNumber)
}

func main() {
	telephoneDirectory := map[string][]string{}

	addToDirectory(telephoneDirectory, "Bob", "12345678")
	addToDirectory(telephoneDirectory, "Bob", "87654321")
	addToDirectory(telephoneDirectory, "Charlie", "20202020")

	fmt.Println(telephoneDirectory) // outputs "map[Bob:[12345678 87654321] Charlie:[20202020]]"
}

This could have been written in a few different ways, but the one I chose in this example is to use the inline existence check to initialise an empty slice of strings for the name if it isn’t found in the directory. The subsequent addition of the number to the corresponding slice thus works the same way whether the name was previously in the directory or not.

A Map of Maps

Sometimes you need multiple dimensions in a map. I don’t have a really good example for this as it’s not a very common use case unless you’re grouping a lot of data for batch processing. So I’ll just show how it’s done:

package main

import "fmt"

func main() {
	myMap := map[string]map[string]int{} // map of string -> (map of string -> int)

	myMap["John"] = map[string]int{} // initialise inner map for key "John"
	myMap["John"]["age"] = 12
	myMap["John"]["height"] = 76

	fmt.Println(myMap) // outputs "map[John:map[age:12 height:76]]"
}

This is quite similar to what we saw in the previous section: the map syntax is rather confusing, and you have to make sure to initialise the inner map properly before using it. Otherwise it starts off as nil and if you try to use it, your program will panic.

Maps of Structs

Instead of maps of maps, it’s more common to have maps of structs. That allows us to look up data records based on some kind of identifier. For instance:

package main

import "fmt"

type Product struct {
	Name  string
	Price float32
}

func main() {

	products := map[string]Product{}

	products["pen"] = Product{
		Name:  "A fine blue pen",
		Price: 12.0,
	}

	fmt.Println(products) // outputs: "map[pen:{A fine blue pen 12}]"
}

However, as I showed in “From .NET to GoLang: Here We Go Again“, there’s a nasty surprise to be seen if you try to update a struct’s field when it’s in a map:

products["pen"].Price = 15.0
Attempting to update a property in a struct in a map causes a compiler error.

This is an unfortunate peculiarity in Go resulting from the concept of addressable values. In short, because values in a map are stored by value (rather than by reference), they can’t be manipulated directly. So there are 2 ways we can carry out this update.

The first is to replace the entire struct. So:

package main

import "fmt"

type Product struct {
	Name  string
	Price float32
}

func main() {

	products := map[string]Product{}

	products["pen"] = Product{
		Name:  "A fine blue pen",
		Price: 12.0,
	}

	products["pen"] = Product{
		Name:  products["pen"].Name,
		Price: 15.0,
	}

	fmt.Println(products) // outputs "map[pen:{A fine blue pen 15}]"
}

The second is to store pointers to products, instead of products by value.

package main

import "fmt"

type Product struct {
	Name  string
	Price float32
}

func main() {

	products := map[string]*Product{}

	products["pen"] = &Product{
		Name:  "A fine blue pen",
		Price: 12.0,
	}

	products["pen"].Price = 15.0

	fmt.Println(products) // outputs "map[pen:0xc000010030]"
}

Note however that this messes up the output when we print the map, because the map is no longer storing products directly. So the value that gets printed out is the address of the Product that the pointer is pointing to.

A Set Data Structure

Go doesn’t have a set data structure (you know, the mathematical kind in which values are unique and unordered). For simple use cases like eliminating duplicates, we can emulate the behaviour of a set using a map:

package main

import "fmt"

func main() {

	numbers := []int{1, 5, 8, 1, 3, 2, 4, 5}

	deduplicated := map[int]struct{}{}

	for _, number := range numbers {
		deduplicated[number] = struct{}{}
	}

	fmt.Println(deduplicated) // outputs "map[1:{} 2:{} 3:{} 4:{} 5:{} 8:{}]"
}

What we’re doing here is creating a map where we only care about the key (and not the value). The use of an empty struct{} is a tip I picked up on Stack Overflow because it doesn’t allocate any memory (as opposed to, for example, a bool). The syntax may appear a little confusing, but when you see two pairs of curly brackets next to each other, think of struct{} as the data type and the second {} as the initialisation syntax.

So then, all we do is feed each number from the slice into the map. As we’ve seen before, assigning another value to a key that already exists will simply overwrite it, leaving a single value for the key. That’s pretty much the same functionality we need for a set.

However, a set can do much more than just deduplicate items. If you need typical set operations such as intersection, union or difference, then check out my article “GoLang Set Data Structure” which shows how to use the third-party golang-set library which should have all the features you need.

Maps and Concurrency

The 2013 official blog post about maps states clearly that maps are not thread-safe, and suggests the use of locks to prevent data races arising from concurrent access to maps.

However, a concurrent version of the map data structure was released in 2017 with Go 1.9, i.e. sync.Map. While I haven’t had the chance to explore it in detail and it’s outside the scope of this article anyway, those looking for such a thing will be pleased to note that it exists and can do the necessary research to learn how to use it.

Summary and Further Reading

The map data structure will be familiar to anyone who has used something similar in other languages. It is easy enough to work with, but does have some quirks of its own that are unique to Go.

Read more about Go maps at the following locations:

Keyboard Movement in Godot

How do you move an object in response to keyboard input in Godot? Read on and find out! This article is similar to “Unity3D: Moving an Object with Keyboard Input“, but for Godot. Also, I literally started learning Godot yesterday, so don’t expect any expert advice!

Setting the Scene

Start by creating a new project. Then, within the “Scene” tab on the left, click the “Other Node” button:

In the modal that comes up next, search for and select a “ColorRect”, then click the “Create” button:

With that, you’ve just added a white square to the top-left of our 2D window. You can optionally change the colour in the Inspector to the right, and/or drag the white square to a more central position:

When you’re happy, press the Play button near the top-right of the Godot editor, or press F5. You’ll be prompted to select the main scene; go with “Select Current”:

Then, you’ll be able to save the current scene (i.e. the lovely square). You can leave the defaults and simply press “Save”:

Once the scene is saved, the game starts running, and you can see what your square would look like in a game:

Creating a Script

In order to move the square when we press a key, we have to add a script. To do this, right-click on the ColorRect in the Scene tab on the left, and select “Attach Script…”:

In the next modal window, leave the defaults and click “Create”:

This opens the Godot script editor, and gives you a little code template written in GDScript to get you started:

Programming Keyboard Movement

We now have a square that we can see in a Godot window, with a script attached to it. We can now program it to do our bidding!

Near the top, just below the “extends ColorRect“, add the following:

var speed = 400

Then, in the “func _process(delta)“, replace “pass” with the following:

	if Input.is_action_pressed("ui_left"):
		position += Vector2.LEFT * delta * speed

Here we’re using the “Input.is_action_pressed()” function to determine whether a key was pressed, and the “ui_left” value specifies that we’re specifically checking whether the left arrow key was pressed. If that’s the case, then we update our square’s position. Here’s a breakdown of that second line:

  • Vector2.LEFT is literally something (a mathematical vector) pointing towards the left.
  • delta is a value that gets passed to the _process() function each time, indicating how much time passed since it was last called. Because framerates can vary, it helps us ensure smooth movement regardless of how the framerate changes.
  • speed is that variable we just defined earlier. We can change that as we like to adjust how quickly the square moves when we press the left arrow key.
  • position is our square’s position. It is itself a vector, and we update it by adding other vectors to it.

This is enough to make the square move to the left when you press the left arrow key, as you can quickly verify if you run it. In the same way, you can support the other arrow keys. Below is what the completed script should look like, also after removing the “_ready()” function which we don’t need here:

extends ColorRect

var speed = 400

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	if Input.is_action_pressed("ui_left"):
		position += Vector2.LEFT * delta * speed
	if Input.is_action_pressed("ui_right"):
		position += Vector2.RIGHT * delta * speed
	if Input.is_action_pressed("ui_up"):
		position += Vector2.UP * delta * speed
	if Input.is_action_pressed("ui_down"):
		position += Vector2.DOWN * delta * speed

At this point, run it (F5 / Play button) again and use the arrow keys to move the square in any direction, including diagonally:

A Better Way

The 2D movement overview page in the official Godot documentation demonstrates an “Input.get_vector()” function that can obtain a single vector representing the combined movement resulting from any of the arrow keys, effectively replacing the need for separate conditional statements. Let’s try that:

extends ColorRect

var speed = 400

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	position += Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") * delta * speed

It works just as well, but it’s a lot more concise! Lovely.

Summary

We’ve seen how to implement basic keyboard movement using Godot. The ColorRect is a 2D element that’s very handy to start playing with. Once we attach a script to it, we can react to keyboard movements using the static Input class. We’ve seen how to do that using two different functions: is_action_pressed(), which tells us whether individual keys have been pressed, and get_vector(), which can check all the ones we want at the same time.

The Ninth Anniversary

Another year has gone by. Considering I’m supposed to be retired from tech blogging, and that I moved country this year, and wrote extremely detailed walkthroughs of Day of the Tentacle and Menzoberranzan, I’m surprised that I managed to write as much as I did.

Among the 26 articles I’ve published over the past year are:

Having stopped working with .NET has clearly opened up the way to more interesting things. Given that the three most popular articles in the last three months were written in the last year, I like to think that at least some of what I’ve written has appealed to a wide audience and is helping them to solve problems.

I’m not sure what the tenth year will be like for Gigi Labs, but I hope it will be fun!

GoLang: Using defer for Scope Bound Resource Management

Most programming languages today provide some way to bind the lifetime of a resource (for instance, a file handle) to a scope within a program, and implicitly clean it up when that scope ends. We can use destructors in C++, using blocks in C#, or with statements in Python. In Go, we use defer. As far as I can tell, this pattern originates in C++ and is called either Resource Acquisition Is Initialization (RAII), or Scope-Bound Resource Management (SBRM). It has various applications that range from resource deallocation (such as file handling, smart pointers, or locks) to scoped utility functions, as I’ve shown in my 2016 article, “Scope Bound Resource Management in C#“.

To understand what we’re talking about and why it’s useful, we first need to start with life without SBRM. Consider a simple program where we read the contents of a file:

package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	file, err := os.Open("example.txt") // open the file
	if err != nil {
		fmt.Println("Failed to open file!")
		return
	}

	data := make([]byte, 4)
	_, err = file.Read(data) // read 4 bytes from the file
	if err != nil {
		fmt.Println("Failed to read the file!")
		return
	}

	fmt.Println(string(data)) // print the data from the file

	file.Close() // close the file

	time.Sleep(time.Second) // pretend to do other work afterwards
}

Here, we’re opening a file, doing something with it, and then (like good citizens) closing it. We’re also doing something afterwards. However, this is in itself quite error-prone. Here are a few things that could happen which would jeopardise that Close() call:

  • We might forget to call Close() entirely.
  • An early return (e.g. due to an error) might skip the call to Close().
  • A more complex function with multiple branching might not reach Close() due to a mistake in the logic.

We can improve the situation by using defer. Putting the Close() call in a defer statement makes it run at the end of the function, whether there was an error or not. To illustrate, we’ll try to open a file that doesn’t exist, and use a Println() instead of the actual Close() to be able to see some output:

package main

import (
	"fmt"
	"os"
)

func main() {
	_, err := os.Open("nonexistent.txt") // open the file
	defer fmt.Println("Closing")
	if err != nil {
		fmt.Println("Failed to open file!")
		return
	}
}

Because the deferred statement runs at the end of the function in any case, we see “Closing” in the output:

Failed to open file!
Closing

defer is useful to ensure resources are cleaned up, but it’s not as good as SBRM constructs from other languages. One drawback is that there’s no actual requirement to use defer when allocating a resource, whereas something like C#’s using block ensures that anything allocated with it gets disposed at the end of its scope.

Another disadvantage is that it is function-scope only. Let’s imagine we have this program where we do a series of time-consuming tasks:

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("Doing hard work...")

	time.Sleep(time.Second)

	fmt.Println("Doing harder work...")

	time.Sleep(2 * time.Second)

	fmt.Println("Doing even harder work...")

	time.Sleep(3 * time.Second)

	fmt.Println("Finished!")
}

We’d like to benchmark each step. In “Scope Bound Resource Management in C#“, I was able to wrap statements in a using block with a utility ScopedTimer that I created. Like C#, Go has blocks based on curly brackets, so let’s try and (ab)use them to measure the time taken by one of the tasks:

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("Doing hard work...")

	{
		startTime := time.Now()
		defer fmt.Printf("Hard work took %s", time.Since(startTime))
		time.Sleep(time.Second)
	}

	fmt.Println("Doing harder work...")

	time.Sleep(2 * time.Second)

	fmt.Println("Doing even harder work...")

	time.Sleep(3 * time.Second)

	fmt.Println("Finished!")
}

The output is:

Doing hard work...
Doing harder work...
Doing even harder work...
Finished!
Hard work took 148ns

Two things went wrong here:

  • The benchmark measured 148 nanoseconds for a task that took one second! That’s because the time.Since(startTime) got evaluated right away rather than when defer started executing its statement.
  • The benchmark for the first task only got printed at the end of the entire function. That’s because defer runs at the end of the function, not at the end of the current scope.

We can fix the first problem by wrapping the deferred statement in an anonymous function that executes itself (which in the JavaScript world would be called an Immediately Invoked Function Expression or IIFE):

// ...

	{
		startTime := time.Now()
		defer func() {
			fmt.Printf("Hard work took %s", time.Since(startTime))
		}()
		time.Sleep(time.Second)
	}

// ...

We now get a benchmark of about 6 seconds, simply because defer is still running at the end of the function:

Doing hard work...
Doing harder work...
Doing even harder work...
Finished!
Hard work took 6.002184229s

To fix this benchmark, we have to fix the second problem, which is that defer runs at the end of the function. What we want is to use defer to measure the duration of each task inside the function. We have a number of ways to do this, but since defer is function scoped, they all involve the use of functions.

The first option is to break up main() into separate functions for each task:

package main

import (
	"fmt"
	"time"
)

func runTask1() {
	startTime := time.Now()
	defer func() {
		fmt.Printf("Hard work took %s\n", time.Since(startTime))
	}()
	time.Sleep(time.Second)
}

func runTask2() {
	startTime := time.Now()
	defer func() {
		fmt.Printf("Harder work took %s\n", time.Since(startTime))
	}()
	time.Sleep(2 * time.Second)
}

func runTask3() {
	startTime := time.Now()
	defer func() {
		fmt.Printf("Even harder work took %s\n", time.Since(startTime))
	}()
	time.Sleep(3 * time.Second)
}

func main() {
	fmt.Println("Doing hard work...")

	runTask1()

	fmt.Println("Doing harder work...")

	runTask2()

	fmt.Println("Doing even harder work...")

	runTask3()

	fmt.Println("Finished!")
}

This does produce correct results:

Doing hard work...
Hard work took 1.000149001s
Doing harder work...
Harder work took 2.001123261s
Doing even harder work...
Even harder work took 3.000039148s
Finished!

However:

  • It is quite verbose.
  • It duplicates all the benchmarking logic.
  • Although many people advocate for smaller functions, I find it easier to read longer functions if the operations are sequential and there’s no duplication, rather than hopping across several functions to understand the logic.

Another way we could do this is by retaining the original structure of main(), but using IIFEs instead of curly brackets to delineate the scope of each task:

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("Doing hard work...")

	func() {
		startTime := time.Now()
		defer func() {
			fmt.Printf("Hard work took %s\n", time.Since(startTime))
		}()
		time.Sleep(time.Second)
	}()

	fmt.Println("Doing harder work...")

	func() {
		startTime := time.Now()
		defer func() {
			fmt.Printf("Harder work took %s\n", time.Since(startTime))
		}()
		time.Sleep(2 * time.Second)
	}()

	fmt.Println("Doing even harder work...")

	func() {
		startTime := time.Now()
		defer func() {
			fmt.Printf("Even harder work took %s\n", time.Since(startTime))
		}()
		time.Sleep(3 * time.Second)
	}()

	fmt.Println("Finished!")
}

It works just as well:

Doing hard work...
Hard work took 1.000069185s
Doing harder work...
Harder work took 2.001031904s
Doing even harder work...
Even harder work took 3.001086566s
Finished!

This approach is interesting because we actually managed to create scopes inside a function where defer could operate. All we did was put each task and its respective benchmarking logic inside an anonymous function and execute it right away. So the sequential code works just the same whether this anonymous function is there or not; it only makes a difference for defer.

Of course, we are still duplicating code in a very uncivilised way here, so we’ll move onto the third approach, which is simply to implement the benchmarking logic in a helper function and use it to execute the task itself:

package main

import (
	"fmt"
	"time"
)

func runBenchmarked(actionName string, doAction func()) {
	fmt.Printf("Doing %s...\n", actionName)
	startTime := time.Now()
	defer func() {
		fmt.Printf("%s took %s\n", actionName, time.Since(startTime))
	}()
	doAction()
}

func main() {
	runBenchmarked("hard work", func() {
		time.Sleep(time.Second)
	})

	runBenchmarked("harder work", func() {
		time.Sleep(2 * time.Second)
	})

	runBenchmarked("even harder work", func() {
		time.Sleep(3 * time.Second)
	})

	fmt.Println("Finished!")
}

The runBenchmarked() function takes care of everything about each task: it prints a message when it’s about to start, executes the task itself, and prints the time it took using the same defer statement we’ve been using for benchmarking. To do this, it takes the name of the task (as a string) as well as the task itself (as a callback function).

Then, in main(), all we need to do is call runBenchmarked() and pass the name of the task and the task itself. This results in the code being brief, free of duplication, and nicely scoped, which I believe is the closest we can get in Go to the SBRM constructs of other languages. The output shows that this works just as well:

Doing hard work...
hard work took 1.000901126s
Doing harder work...
harder work took 2.001077689s
Doing even harder work...
even harder work took 3.001477287s
Finished!

Conclusion

defer in Go provides some degree of SBRM support for scoped cleanup or utility purposes. However, it suffers from the following drawbacks:

  • It does not enforce implicit cleanup of allocated resources as similar constructs in other languages do.
  • Any parameters that need to be evaluated at the end of the scope should be wrapped in an IIFE.
  • It is function-scoped, therefore using defer for a limited/block scope inside a function requires it to be wrapped in another function.

"You don't learn to walk by following rules. You learn by doing, and by falling over." — Richard Branson