Tag Archives: Go

Getting Started with the Go Programming Language (GoLang) using VS Code

Go is a relatively young programming language, launched in 2009, with simplicity at its heart. Whereas it might not have all the bells and whistles you might expect from a modern programming language (see “From .NET to GoLang: Where Did Everything Go?” and “From .NET to GoLang: Here We Go Again” for my critique), it has some features that make it unique, such as its first-class concurrency model using channels and goroutines, based on Tony Hoare’s 1978 Communicating Sequential Processes (CSP).

You can set up Go and write your first program easily in a couple of minutes, but in order to program comfortably, I recommend using an Integrated Development Environment (IDE) that you can use to debug your code. You can use whatever you like, but I’ll show you how to use Visual Studio Code (VS Code) to write and debug Go code.

Download and Install Go

First, you need to download and install Go. This link has all the instructions you need for Linux, Mac and Windows, and they’re pretty straightforward, so I won’t bother repeating all of them here.

If you’re on Linux, though, do note that the first command is actually two commands combined with an && operator, so you need to add sudo before both of them. To make it super clear, you need to do the following steps on Linux:

  1. Download the latest Go package for Linux.
  2. Run the following to remove any previous Go installation (skip this if you’ve never installed Go before):
    • sudo rm -rf /usr/local/go
  3. cd into the directory where you downloaded the Go package.
  4. Run the following to extract the Go package and put it in the right folder:
    • sudo tar -C /usr/local -xzf go1.20.3.linux-amd64.tar.gz
    • Be sure to change the filename to the one you actually downloaded.
  5. Add the following to the end of ~/.profile:
    • export PATH=$PATH:/usr/local/go/bin
    • Restart your computer for the change to take effect.
  6. Run go version to ensure that the go command is now accessible in your terminal.

A Minimal Example

Create a new folder, and name it whatever you like (e.g. “hellogo”). In it, add a file called main.go, and add the following code in it:

package main

import "fmt"

func main() {
    fmt.Println("Hello Go!")
}

Using Go from the Command Line

You can run the above program in your favourite terminal by cd’ing into the directory where you added main.go, and running the following command:

go run main.go

The output, as you would expect, is:

Hello Go!

If you want to compile the code into an executable, you can run the following command:

go build main.go

This generates an executable named main. If you want to specify the name of the executable, you can add a -o switch and provide the name you want. For instance, the following command generates an executable named myapp:

go build -o myapp main.go

It’s important to note that the file being compiled must be at the end of the command, so the -o switch goes before it.

To run the executable:

  • On Windows, run: myapp
  • On Linux or Mac, run: ./myapp

Setting up VS Code for Go

If you don’t have it already, download and install Visual Studio Code. Use it to open the folder where main.go is. At this point, we need to install 3 things:

  1. The Go extension for Visual Studio Code
  2. The Delve debugger
  3. gopls, the Go language server

In my experience, VS Code automatically prompts you to install these when you open a .go file in it, so the easiest way is to just accept. In case it doesn’t, you can install the Go extension yourself from the Extensions tab on the left side of VS Code, and you can install the other two by following their respective instructions.

VS Code asks whether you want to install the Go extension. Click “Install”.
You can also find the Go extension from the Extensions tab on the left. After installing it, VS Code helpfully asks you to install dlv (Delve) and gopls, which you also need.
When you accept to install Delve and gopls, you can see their progress and status in the Output window at the bottom of VS Code.

Debugging Go

Once you’ve installed all three items from the previous section, VS Code becomes a handy tool to write, run and debug Go code with ease. In fact, you get things like syntax highlighting, code completion, and the ability to debug your code.

Code completion for Go code.

To run/debug your code directly from VS Code, you can press F5, or do it via the “Run and Debug” tab (which looks like a Play button with a beetle) on the left side of VS Code. Doing this immediately gives us an error:

The error says: “go mod file not found in current directory or any parent directory”.

That’s because we missed a step. In the bottom part of VS Code, switch to the integrated terminal (“Terminal” tab) or, if you don’t have one, open a new one via the Terminal menu at the top of VS Code. Then, run the following command:

go mod init main

This creates a go.mod file:

Running go mod init main creates a go.mod file, initialising the main module.

You can now press F5 again to debug Go. You can set a breakpoint by clicking next to a line number; this will show the breakpoint as a red circle, and the code will stop when it reaches that point, allowing you to step through the code, inspect variables, and see other parts of the state of the program:

Hitting a breakpoint while debugging Go code.

There isn’t much to debug in this simple program, but at least this gives you what you need to be able to debug more interesting programs as you write them. Debugging is an entire topic in itself that’s out of scope for this article, but it’s an essential skill for a competent developer. You can learn more about it from:

One other tip: while you are in a debugging session, VS Code creates a temporary executable called __debug_bin. If you’re using source control, be sure to exclude it (e.g. add it to your .gitignore file when using Git).

Conclusion

Go is fairly straightforward to set up and use:

  • Download Go and follow the official instructions to set it up.
  • Use Go from the command-line if you need to (e.g. for Continuous Integration).
  • Set up the Go extension, Delve debugger and gopls language server in VS Code.
  • Press F5 to debug your Go programs in VS Code!

That’s all you need to keep Going!

Working with VS Code Launch Configurations

Visual Studio Code (VS Code) is a wonderful IDE. I’m not generally known to praise Microsoft’s products, but VS Code lets me develop and debug code productively in all the languages I’ve needed to work with, on any operating system. I don’t even use Visual Studio any more.

Launch configurations are at the heart of debugging with VS Code. In this article, I’ll explain how you can debug code using different languages, even at the same time. I’ll also show you how you can customise these launch configurations to pass command-line arguments, set environment variables, run pre-launch tasks, and more.

Getting Started with Launch Configurations

Anytime you want to debug something, you just press F5 (like in Visual Studio). Initially, you probably won’t have any launch configurations set up, in which case you’ll be prompted to choose what kind of launch configuration you want to create, as shown in the screenshot below. This list might vary depending on the language runtimes you have installed. When you select one, you’ll be guided towards creating a sample launch configuration.

Pressing F5 brings up a list of languages for which you can create launch configurations.

Another way is to click on the Debug tab on the left, which looks like a play button. You can then click the link to “create a launch.json” file, shown in the screenshot below. We’ll see this in practice in the next sections as we create launch configurations for various languages.

The Debug tab allows you to “create a launch.json file”.

Debugging Python

Before we debug anything, we need some code. Let’s add a folder with a Python file in it, and add the following code:

import time
import datetime

while True:
    time.sleep(1)
    print(f"Swiss church bells say it's {datetime.datetime.now()}")

If I press F5, this actually works for me out of the box:

Pressing F5 runs the Python program and lets us debug it.

Clicking next to a line number adds a breakpoint. When the program hits that point, it pauses and allows us to inspect variables and other things. You can press F10 to go to the next statement, F11 to step into a function call, and Shift+F11 to step out. While a tutorial on how to debug code is outside the scope of this article, if you’re unfamiliar with debugging, this should at least be enough to get you started.

After hitting a breakpoint in the Python program, we can advance step by step and see the state of local variables.

A Launch Configuration for Python

Follow either of the methods in the earlier “Getting Started” section to create your first Python launch configuration. This creates a launch.json file under a .vscode folder with the following contents. As launch.json may contain personally customised configurations for different developers (e.g. with different input parameters), it’s best not to commit it to source control.

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Current File",
            "type": "python",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal",
            "justMyCode": true
        }
    ]
}

There isn’t much to it: this will simply run whatever file you have open in VS Code (which is what ${file} means). This is useful if you want to run specific files (I do this with tests in Go for instance), but not so much if you have a program with a single entry point and want to run that regardless of what you have open in VS Code. In that case, it’s easy to change the program value:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Main File",
            "type": "python",
            "request": "launch",
            "program": "${workspaceFolder}/pyticker/main.py",
            "console": "integratedTerminal",
            "justMyCode": true
        }
    ]
}

${workspaceFolder} represents the folder we have open in VS Code, so this makes sure we’re relative to that. ${workspaceFolder}, ${file} and other such variables are documented in VS Code’s Variables Reference.

Command Line Arguments

Let’s modify our Python program as follows:

import time
import datetime
import sys

while True:
    time.sleep(int(sys.argv[2]))
    print(f"{sys.argv[1]} church bells say it's {datetime.datetime.now()}")

The program now takes the nationality of the church bells as well as the sleep interval from command-line arguments. We’re not doing validation or error-handling for the sake of brevity. The following is an example of how to execute this successfully from a terminal:

$ python3 main.py Maltese 5
Maltese church bells say it's 2023-02-15 19:04:28.311805
Maltese church bells say it's 2023-02-15 19:04:33.315787
Maltese church bells say it's 2023-02-15 19:04:38.319811

To pass the same command-line arguments when debugging with VS Code, we add args to the launch configuration:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Main File",
            "type": "python",
            "request": "launch",
            "program": "${workspaceFolder}/pyticker/main.py",
            "console": "integratedTerminal",
            "justMyCode": true,
            "args": ["Maltese", "5"]
        }
    ]
}

Multiple Launch Configurations

As you no doubt noticed, launch.json contains a JSON array of configurations. That means it’s very easy to add more of these configurations, for instance, when you need to provide different inputs:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python Ticker (Maltese)",
            "type": "python",
            "request": "launch",
            "program": "${workspaceFolder}/pyticker/main.py",
            "console": "integratedTerminal",
            "justMyCode": true,
            "args": ["Maltese", "5"]
        },
        {
            "name": "Python Ticker (Swiss)",
            "type": "python",
            "request": "launch",
            "program": "${workspaceFolder}/pyticker/main.py",
            "console": "integratedTerminal",
            "justMyCode": true,
            "args": ["Swiss", "1"]
        }
    ]
}

From the Debug tab, you can then select the configuration you want to run from the drop-down before hitting F5 to debug with that configuration:

The drop-down in the Debug tab lets you select which Launch Configuration you want to debug.

A Launch Configuration for Node.js

Let’s add a new folder as a sibling to our Python folder, and add the following code in a new app.js file, completely disregarding cleanup for the sake of brevity and YOLO:

const fs = require('fs');

setInterval(() => {
    message = `Swiss church bells say it's ${new Date()}`;
    console.log(message)
    fs.appendFileSync('ding-dong.txt', message + '\n');
}, 1000);

We can set up a simple launch configuration to run this in launch.json:

...
    "configurations": [
        {
            "name": "Node.js Ticker",
            "program": "${workspaceFolder}/jsticker/app.js",
            "request": "launch",
            "type": "node"
        },
...

It works, writing output every second both to standard output and a file:

The Node.js program’s output can be seen in the Debug Console as well as the output file. The overall folder structure is also shown on the left.

The only problem is that the file was created top-level, and since we only specified a filename in the code (not a folder or path), then this must mean that Node.js is running with the top-level folder in VS Code as the current working directory. We can change that in the launch configuration using cwd:

...
    "configurations": [
        {
            "name": "Node.js Ticker",
            "program": "${workspaceFolder}/jsticker/app.js",
            "request": "launch",
            "cwd": "${workspaceFolder}/jsticker",
            "type": "node"
        },
...

When we run this again, the file is created under the jsticker folder.

Pre-Launch Tasks

Sometimes you want to run something before starting your program. For instance, in my work setup, I generate Swagger docs and perform other prerequisite tasks. But since we’re not doing anything that fancy, we’ll just delete the ding-dong.txt file every time we run the “Node.js Ticker” launch configuration.

To do this, we first need to add a tasks.json file inside the .vscode folder, next to launch.json, and add the following to it:

{
    "version": "2.0.0",
    "tasks": [
      {
        "label": "rmfile",
        "command": "rm",
        "args": ["ding-dong.txt"],
        "options":{
            "cwd": "${workspaceFolder}/jsticker"
        }
      }
    ]
  }

This defines a task called rmfile that will run rm ding-dong.txt from the jsticker folder. We then refer to this task in the relevant launch configuration using preLaunchTask:

...
        {
            "name": "Node.js Ticker",
            "program": "${workspaceFolder}/jsticker/app.js",
            "request": "launch",
            "cwd": "${workspaceFolder}/jsticker",
            "preLaunchTask": "rmfile",
            "type": "node"
        },
...

A Launch Configuration for Go (GoLang)

For Go, we need to:

  1. Run go work init from the top-level folder opened in VS Code
  2. Create a new folder alongside pyticker and jsticker
  3. Run go mod init main in it
  4. Add the following code to a new main.go file:
package main

import (
	"fmt"
	"time"
)

func main() {
	for {
		now := time.Now()
		fmt.Printf("Swiss bells won't stop ringing! It's %s!\n", now)
		time.Sleep(time.Second)
	}
}

We can now add a launch configuration for this Go program:

...
        {
            "name": "Go Ticker",
            "type": "go",
            "request": "launch",
            "mode": "debug",
            "program": "${workspaceFolder}/goticker/main.go"
        },
...

And we can have lots of fun running and debugging it, as a reminder that Swiss church bells need to tell you the time all the time. It’s not like they sell watches in Switzerland or anything.

Our Go program runs via the “Go Ticker” launch configuration.

Environment Variables

As we’ve seen with the Python example, however, other countries also have church bells. Instead of using command-line arguments to customise the output, we’ll instead pass environment variables. First, we need to modify the code a little:

package main

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

func main() {
	for {
		country := os.Getenv("COUNTRY")
		intervalSecsStr := os.Getenv("INTERVAL_SECS")
		intervalSecs, _ := strconv.Atoi(intervalSecsStr)
		now := time.Now()
		fmt.Printf("%s bells won't stop ringing! It's %s!\n", country, now)
		time.Sleep(time.Duration(intervalSecs) * time.Second)
	}
}

Then we add the relevant environment variables as key-value pairs in an env block in the launch configuration:

...
        {
            "name": "Go Ticker",
            "type": "go",
            "request": "launch",
            "mode": "debug",
            "program": "${workspaceFolder}/goticker/main.go",
            "env": {
                "COUNTRY": "Maltese",
                "INTERVAL_SECS": "3"
            }
        },
...

When you run it again, it makes all the difference:

The Go program runs with environment variables set.

Go Build Flags

Go has some specific build flags that can’t be passed as regular command-line parameters, such as build tags or the Data Race Detector. If you want to use these, you’ll have to pass them via buildFlags instead:

...
            "buildFlags": "-race",
...

Running Multiple Programs with Compounds

It’s common to need to run multiple programs at once, especially in a microservices architecture, or if there are separate backend and frontend applications. This can be done in VS Code using compound launch configurations. For instance, if we wanted to run both the Python and Go programs, we could define a compound as follows (compounds go after configurations in launch.json):

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Go Ticker",
            "type": "go",
            "request": "launch",
            "mode": "debug",
            "program": "${workspaceFolder}/goticker/main.go",
            "env": {
                "COUNTRY": "Maltese",
                "INTERVAL_SECS": "3"
            }
        },
        {
            "name": "Node.js Ticker",
            "program": "${workspaceFolder}/jsticker/app.js",
            "request": "launch",
            "cwd": "${workspaceFolder}/jsticker",
            "preLaunchTask": "rmfile",
            "type": "node"
        },
        {
            "name": "Python Ticker (Maltese)",
            "type": "python",
            "request": "launch",
            "program": "${workspaceFolder}/pyticker/main.py",
            "console": "integratedTerminal",
            "justMyCode": true,
            "args": ["Maltese", "5"]
        },
        {
            "name": "Python Ticker (Swiss)",
            "type": "python",
            "request": "launch",
            "program": "${workspaceFolder}/pyticker/main.py",
            "console": "integratedTerminal",
            "justMyCode": true,
            "args": ["Swiss", "1"]
        }
    ],
    "compounds": [
        {
            "name": "Go and Python Tickers",
            "configurations": ["Go Ticker", "Python Ticker (Swiss)"]
        }
    ]
}

The compound simply refers to the individual launch configurations by name, and has a name of its own. It can then be selected from the drop-down in the Debug tab just like any other launch configuration. By running a compound, you can debug and hit breakpoints in any application that is part of that compound.

In fact, you’ll notice there is a new drop-down next to the debugging buttons towards the central-top part of the screen. This allows you to switch certain views (e.g. the Debug Console) between running applications.

A compound can be selected from the list of launch configurations in the Debug tab. When it is run, a dropdown appears next to the debugging buttons in the top part of the screen.

Conclusion

We’ve seen that Launch Configurations allow you to run and debug applications in VS Code with great flexibility, including:

  • Run the current file or a specific one
  • Set the current working directory
  • Run different programs
  • Use different programming languages
  • Set up different configurations even for the same program
  • Pass command-line arguments
  • Set environment variables
  • Run/debug multiple programs at the same time

This provides a great IDE experience even for more complex application architectures in a monorepo.

From .NET to GoLang: Here We Go Again

Although I’ve been working with Go for less than a year, there are a number of things I’ve learned or noticed since I wrote “From .NET to GoLang: Where Did Everything Go?“, my critique of the Go programming language and ecosystem, three and a half months ago.

As a result, I’d like to use this followup article to discuss some new things that bother me as well as revisit points I already mentioned in the original article.

This is also the 300th article published at Gigi Labs. Hooray!

Productivity

I ended “From .NET to GoLang: Where Did Everything Go?” on a somewhat negative note:

“Coming from C#, learning my way around Go has been a fun diversion, but also something of a disappointment. Given the simplistic, verbose and sometimes confusing language, and the limited standard library, there’s no reason I can think of to choose Go for a new project over something richer and more mature like C#, which nowadays runs on any platform.”

As a result, it is somewhat surprising that I actually feel a lot more productive with Go than I ever did with C#, despite C# being the superior language in my opinion. I think the main reason for this is that .NET software tends to be extremely overcomplicated, owing to a combination of excessive OOP, layers of abstraction, design patterns, and obscenely exploited third party libraries (see “Pitfalls of AutoMapper“) that overzealous software engineers anxious to prove their mettle tend to sprinkle all over the place. On the other hand, Go’s simplicity – the very thing I was complaining about – does not provide many opportunities for this kind of madness.

Stability

I also mentioned that Go’s development seems to be somewhat slow, with particular reference to generics having taken 12 years to appear in the language. But this is also in a way a good thing. Go is a very stable language and upgrading to newer versions is a breeze. I can’t say the same about C# any more, given that every year it’s become a hassle to keep up with the latest unnecessary language ‘features’ as well as breaking changes to ASP .NET Core.

Exception Handling

I talked about Go’s lack of exception handling and the need to have heaps of if statements to check errors returned by function calls.

Go does in fact have a mechanism similar to exception handling, in the form of panic and recover. But since there are no exception objects, this mechanism operates with basic types, just like how the error interface returns a string and does not otherwise carry structured error information. This is unfortunate because it means it’s impossible for Go to have first chance exceptions, which make the debugger pause as soon as an exception is thrown – an essential troubleshooting tool that I sorely miss from .NET.

Defer

The same blog post I linked about panic and recover also talks about defer, which is the closest thing Go has to .NET’s using block (to clean up resources that need disposing). In “Scope Bound Resource Management in C#“, I discuss many of the things you can do by using and abusing the cleanup mechanisms in C++ and C#, and you can do most if not all of the same things in Go with defer.

Exported Names

Go code is organised in packages (like namespaces), but it doesn’t have public or private accessors. Variables and functions can be shared between packages if they start with a capital letter.

Wait, what? Let’s see this in action. I made a really simple program with two packages. I call a function from the money package from main():

Make() can be called from main() because it starts with a capital letter.

But if we change the name to start with a lowercase letter, it’s no longer visible:

make(), on the other hand, can only be used by code inside the money package.

I’m not at all a fan of such magic conventions, since they are not obvious at all to newcomers (unlike a keyword such as public would be), in the same way that I’m also not a fan of convention over configuration (used for a long time in ASP .NET Web API, MVC and Core) and content before chrome (used by the disastrous Windows 8 “Metro” theme).

LINQ and Sets

In “From .NET to GoLang: Where Did Everything Go?“, I complained about the standard library’s lack of support for common data structures such as sets, as well as the lack of anything like LINQ, making Go rely a lot on loops and elementary data structures.

However, in both cases, the community has stepped in and filled the gaps. I recently wrote the article “GoLang Set Data Structure“, which describes how to use the third party golang-set library. There is also a go-linq library on GitHub which I haven’t tried yet. Although it’s likely very useful, it’s somewhat unfortunate that Go’s syntax makes its usage a lot less elegant than in C#.

Slices

Go has arrays and slices (the latter being an array-like data structure). You can get a sub-range (also called “slice”) of an array, slice or string just like you can in Python:

package main

import "fmt"

func main() {
	numbers := []int{1, 2, 3, 4, 5, 6}
	jonathan := "Jonathan"

	fmt.Println(numbers[1:3]) // [2 3]
	fmt.Println(jonathan[:3]) // Jon
}

Go, however, doesn’t allow negative indices:

package main

import "fmt"

func main() {
	numbers := []int{1, 2, 3, 4, 5, 6}

	fmt.Println(numbers[1:-3]) // compiler error
}

Whereas with Python you can use negative indices to denote an index starting from the end (handy when you want to e.g. trim off the last character of a string):

>>> numbers = [1, 2, 3, 4, 5, 6]
>>> numbers[1:-3]
[2, 3]

Achieving the same thing with Go is a little more verbose, which is not very surprising at this point:

package main

import "fmt"

func main() {
	numbers := []int{1, 2, 3, 4, 5, 6}

	fmt.Println(numbers[1 : len(numbers)-3]) // [2 3]
}

Else Braces

I did mention in the original article that Go is a little fussy about braces used with an if statement, both in terms of their necessity and placement. Where it gets really annoying, though, is when it fails to build because you put the else on the next line, like this:

package main

import "fmt"

func main() {
	if ("sky" == "blue") {
		fmt.Println("Not in Ireland")
	}
	else { // compiler error
		fmt.Println("Probably Ireland")
	}
}

The pedantic bastard requires you to put the else on the same line as the preceding brace, like this:

package main

import "fmt"

func main() {
	if "sky" == "blue" {
		fmt.Println("Not in Ireland")
	} else { // compiler is happy
		fmt.Println("Probably Ireland")
	}
}

Whatever makes you happy, dear Go compiler.

Syntax (Revisited)

I wrote at length in the original article about how I find Go’s variable declaration syntax to be very confusing at best. There’s an article on the Go blog explaining the reasoning behind Go’s Declaration Syntax, which is informative but frankly not very convincing.

I also wrote about my aversion to the := syntax. My feelings are not just a matter of style but also come from having been screwed a couple of times by this fancy operator. Let’s see a simple if somewhat absurd example:

package main

import "fmt"

func uptown() (string, error) {
	return "Uptown Funk!", nil
}

func main() {
	lyrics := "Eye of the Tiger"

	if "sky" != "fuchsia" {
		lyrics, err := uptown()
		if err != nil {
			// nobody cares, Bruno
		}
		fmt.Println(lyrics)
	}

	fmt.Println(lyrics)
}

What do you think the output will be? OK, I’ll tell you. It’s:

Uptown Funk!
Eye of the Tiger

I used the := operator because the err variable is new and needs declaring. But because of that same operator, the lyrics variable inside the if statement is an entirely new variable that shadows the one outside. So once we’re back outside the if statement, thinking we updated the value of the lyrics variable, we find that it still has its original value.

I’m not even sure what is the best way to deal with this situation, though an easy way to update the outer lyrics variable is to use an intermediate variable to capture the output of uptown() and then assign that to lyrics:

package main

import "fmt"

func uptown() (string, error) {
	return "Uptown Funk!", nil
}

func main() {
	lyrics := "Eye of the Tiger"

	if "sky" != "fuchsia" {
		innerLyrics, err := uptown()
		if err != nil {
			// nobody cares, Bruno
		}
		fmt.Println(innerLyrics)
		lyrics = innerLyrics
	}

	fmt.Println(lyrics)
}

Addressable Values

Let’s say I have a map whose value is a struct. For instance:

package main

type Person struct {
	Hobby string
}

func main() {
	people := map[string]Person{"James": {Hobby: "Drinking"}}

	people[i].Hobby = "Farting"
}

I create a map with a single value which maps the key “James” to a struct, and then I try to update a property on that struct. Seems simple enough? And yet, it doesn’t compile:

You cannot simply assign a value to a struct’s property in a map in Go. Sorry.

What?! Why? It seems like this boils down to this concept of Addressable values in Go, WHICH FRANKLY I DON’T CARE ABOUT BECAUSE NO OTHER LANGUAGE EVER HAD A PROBLEM UPDATING A PROPERTY LIKE THIS!

To update that value, as far as I can tell, you instead have to take the detour of replacing the entire object in the map:

func main() {
	people := map[string]Person{"James": {Hobby: "Drinking"}}

	updatedJames := Person{Hobby: "Drinking"}
	people["James"] = updatedJames
}

Dates & Times

In “GoLang Timestamps: A Cross-Platform Nightmare“, I wrote about a very specific problem I encountered with Go time. But why don’t we talk about something more common? Parsing timestamps.

Let’s try to parse a date/time in a way that any reasonable language would allow, i.e. using placeholders like “yyyy-MM-dd“:

You cannot simply parse a date in an intuitive manner either.

No, Go doesn’t use such placeholders. Why would it? Instead, it uses this crazy mnemonic device to represent the various parts of the date:

Mon Jan 2 15:04:05 MST 2006

If you look closely, you’ll notice that these are actually the numbers 1 through 7:

  • Jan (1) is the month
  • 2 is the day
  • 15 (3) is the hour
  • 4 is the minutes
  • 5 is the seconds
  • 2006 (6) is the year
  • MST (-7) is the time zone relative to UTC

So, the equivalent of parsing as “yyyy-MM-dd” would be something like:

You have to use the weird mnemonic thingy. But you have to chop off the extra text too.

Oops, that didn’t work either. I have to chop off the extra bits for it to work:

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println(time.Now()) // 2022-11-16 22:23:39.354436468 +0100 CET m=+0.000014317
	parsedTime, err := time.Parse("2006-01-02", "2022-11-16")
	if err != nil {
		fmt.Println("Failed to parse time: ", err)
	} else {
		fmt.Println("Parsed time: ", parsedTime)
	}
}
Finally. That was a little bit complicated for such a basic operation.

Fortunately, there are a few predefined layouts you can use, such as time.RFC3339. But the ubiquitous RFC 8601 is nowhere to be found, so you’ll likely have to beg Stack Overflow for that.

Conclusion

It’s certainly been educational, entertaining and occasionally frustrating to learn about Go’s quirks. And considering I haven’t been working with it that long, I’m sure there will be lots more to come!

GoLang Timestamps: A Cross-Platform Nightmare

Have you ever had one of those bugs where your tests failed in your Continuous Integration (CI) pipeline, but they worked just fine locally, and the only way to troubleshoot the problem was to add logs and wait significant amounts of time for each run to fail? Well, I had one of those this week, and I learned a thing or two in the process. I’m hoping to share that here for anyone else who might run into the same problem.

Time Precision Across Operating Systems

Let’s create a really simple Go program.

  1. Create a new folder.
  2. In that folder, run go mod init main
  3. Create a file called main.go and paste the following code:
package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println(time.Now())
}

Nothing special, right? It just prints out the current time. Let’s run it then.

The following is the output on my personal laptop running Linux:

$ go run main.go
2022-10-27 17:34:43.471537065 +0200 CEST m=+0.000016185

And the following is the output on my work machine, which is a Mac:

$ go run main.go
2022-10-27 17:35:27.349114 +0200 CEST m=+0.000181459

See the difference? The Linux output was precise to the nanosecond (9 fractional digits of a second), whereas the Mac output was precise to the microsecond (6 fractional digits of a second). This subtle difference was the root cause of my problem.

How Did It Happen?

I had written automated tests to verify some simple CRUD functionality. I would send the data to an API, and the API would store it in a PostgreSQL database. Then, I would retrieve the data and verify that it matched what I had sent. Sounds trivial? Yes, but it failed. It failed on the CI server where I couldn’t debug the problem as easily as I could on my machine.

The reason it worked on my Mac was that a timestamp field in PostgreSQL has microsecond precision. Coincidentally, that’s the same precision that I get for Go time on my Mac.

However, on the CI system as well as on a colleague’s WSL system, things worked differently:

  1. The test data, as well as the expected values to be compared later, are generated in Go. Timestamps have nanosecond precision.
  2. The data is sent via the API and stored in the database. There’s a loss of precision and the timestamps are stored with microsecond precision.
  3. The test retrieves, via the API, the same data that has just been saved. The timestamps come back with microsecond precision.
  4. The automated test compares the expected values (nanosecond precision) with the actual values (microsecond precision) and finds that they are different. The test fails.

How Did I Resolve It?

Honestly, I don’t know what the right way to solve this problem is. It’s a pretty messy situation reminiscent of floating-point error, and that in fact influenced my solution.

The first thing I did was strip off the extra precision. Go time has this Truncate() function you can use for exactly this reason. For instance, let’s try the following:

func main() {
	fmt.Println(time.Now())
	fmt.Println(time.Now().Truncate(time.Microsecond))
}

The output on my Linux laptop then looks like:

$ go run main.go
2022-10-27 19:06:52.427287572 +0200 CEST m=+0.000013810
2022-10-27 19:06:52.427347 +0200 CEST

Doing this on my Mac would, I suppose, mean that the Truncate() is a no-op and just returns the same thing – at least that’s the behaviour I observed when testing it out.

However, in practice, Truncate() alone didn’t solve the problem. Some tests kept failing intermittently because the last digit (the microsecond) was off by one. I’m not sure what was causing this – perhaps some kind of rounding, e.g. when the data goes into the database.

So I used the same approach as with floating-point equality comparisons and decided to allow an error of one microsecond, i.e. if two timestamps are one microsecond apart, we assume they’re still equal. This is as simple as using Sub() to calculate the interval between two timestamps, and consider them equal if it’s not greater than one microsecond:

func timestampsEqual(a, b time.Time) bool {
	return b.Sub(a) <= time.Microsecond
}

Let’s test it out with some code:

func main() {
	t1 := time.Now().Truncate(time.Microsecond)
	t2 := t1.Add(time.Microsecond)
	t3 := t2.Add(time.Microsecond)

	fmt.Println("t1 == t2: ", timestampsEqual(t1, t2))
	fmt.Println("t1 == t3: ", timestampsEqual(t1, t3))
}

And the output would be:

$ go run main.go
t1 == t2:  true
t1 == t3:  false

So, it’s not great, and I’m sure there are ways to improve, but it seems to have solved my problem at least. One thing I’d like to do is to use some equivalent of math.abs() so that the order of the timestamps doesn’t matter. But, I have no idea how to do that with Go time with out-of-the-box functionality.

GoLang Set Data Structure

One of my complaints about Go in my recent article, “From .NET to GoLang: Where Did Everything Go?“, was the lack of built-in data structures, such as a set.

Many people suggest writing your own, and there are websites that show you how to do this. Of course, you can use a map to represent a set, but you’d also have to implement common operations such as union, intersection and difference.

Fortunately, there’s no need to reinvent the wheel like this, because the community has stepped in to provide a good set implementation. The golang-set package provides all the functionality expected of a set data structure, and its v2 release makes full use of the (recently released) Go generics functionality, allowing you to use it with virtually any data type.

I’m expecting the reader to know what a set is (i.e. an unordered, deduplicated collection) and what the common operations are. I’ll be focusing on how to work with sets in Go using the aforementioned package.

Installing golang-set

First, as usual, set up a Go module:

go mod init main

Then, to install golang-set in your Go project, make sure to get the v2 release, as follows:

go get github.com/deckarep/golang-set/v2

Then in your Go file (e.g. main.go) be sure to import the package as in the following example:

package main

import (
    mapset "github.com/deckarep/golang-set/v2"
)

func main() {
	// test code will Go here
}

Initialising a Set

You create a new Set by calling the NewSet() function. For instance, for a set of strings:

fruit := mapset.NewSet[string]()

Since the Set type supports generics, you can use a different type instead of a string, for instance, we could have a set of integers:

oddNumbers := mapset.NewSet[int]()

While we’ll soon see how to add values to our set, it’s possible to initialise a set with values when you create it. For instance, you can pass integer values directly:

oddNumbers := mapset.NewSet(1, 3, 5, 7, 9)

Or else unpack an array of integer values using the ... spread operator:

	oddNumbersArray := []int{1, 3, 5, 7, 9}
	oddNumbers := mapset.NewSet(oddNumbersArray...)

Note that in these cases, it’s not necessary to specify the generic type parameter when calling NewSet() because Go is smart enough to figure out the type from the values you pass in.

String Representation of a Set

The Set type has a convenient string representation. You can see this by printing the Set directly, e.g.:

fmt.Println(oddNumbers)

…gives…

Set{1, 3, 5, 7, 9}

Alternatively, you can obtain the same string representation programmatically. The following should provide the same output:

	oddNumbersStr := oddNumbers.String()
	fmt.Println(oddNumbersStr)

Actually, it won’t necessarily give exactly the same output. Because the Set is unordered, the order of the values in the string representation may change. For instance, running it again, you might get this:

Set{9, 1, 3, 5, 7}

Converting a Set to a Slice

You can use the ToSlice() function to convert a Set to a slice:

	oddNumbersSlice := oddNumbers.ToSlice()
	fmt.Println(oddNumbersSlice) // prints "[5 7 9 1 3]"

Update 9th August 2022: Like String(), the result of ToSlice() is non-deterministic and can give different results each time it’s run. This is particularly annoying when you want to compare sets in a test (the set data might be coming from an API, so it’s not just a matter of calling Equal()).

Adding and Removing Items

Use the Add() function to add items to a set. For example:

	evenNumbers := mapset.NewSet[int]()
	evenNumbers.Add(2)
	evenNumbers.Add(4)
	evenNumbers.Add(6)
	evenNumbers.Add(8)
	evenNumbers.Add(10)

	fmt.Println(evenNumbers)

It seems like this needs to be called for each item you want to add. It would be nice to have something like C#’s AddRange() method, by which you could append an entire array, list, or other collection of items.

You can remove a specific item from a Set by calling Remove():

	evenNumbers.Remove(10)

	fmt.Println(evenNumbers)

The combined output of these two examples would be something like:

Set{2, 4, 6, 8, 10}
Set{2, 4, 6, 8}

Another way to remove items from a set is to call Pop(), which “removes and returns an arbitrary item from the set.” I suppose this could be useful when you want to process all items in the set until it’s empty.

You can also call Clear() to remove all items from the set.

Counting Items

Use the Cardinality() function to get the number of items in the set. The following example outputs a value of 4.

fmt.Println(evenNumbers.Cardinality())

Set Membership

You can test basic set membership using Contains():

	primeNumbers := mapset.NewSet(2, 3, 5, 7, 11)

	fmt.Println(primeNumbers.Contains(3)) // prints "true"
	fmt.Println(primeNumbers.Contains(4)) // prints "false"

Set Comparisons

The following functions are available to compare sets:

  • Equal() returns true if the two sets have the same elements
  • IsSubset() returns true if the first set is a subset of the second set, or if they’re equal
  • IsProperSubset() returns true if the first set is a subset of the second set and they aren’t equal
  • IsSuperset() returns true if the first set is a superset of the second set (i.e. the second set is a subset of the first), or if they’re equal
  • IsProperSuperset() returns true if the first set is a superset of the second set (i.e. the second set is a subset of the first), and they aren’t equal

The following example shows what to expect from these operations:

	primeNumbers := mapset.NewSet(2, 3, 5, 7, 11)
	primeNumbers2 := mapset.NewSet(2, 3, 5, 7, 11)
	primeNumbersSubset := mapset.NewSet(2, 3, 5)

	// set equality

	fmt.Println(primeNumbers.Equal(primeNumbers2))      // prints "true"
	fmt.Println(primeNumbers.Equal(primeNumbersSubset)) // prints "false"

	// subset

	fmt.Println(primeNumbersSubset.IsSubset(primeNumbers)) // prints "true"
	fmt.Println(primeNumbers.IsSubset(primeNumbersSubset)) // prints "false"
	fmt.Println(primeNumbers2.IsSubset(primeNumbers))      // prints "true"

	// proper subset

	fmt.Println(primeNumbersSubset.IsProperSubset(primeNumbers)) // prints "true"
	fmt.Println(primeNumbers.IsProperSubset(primeNumbersSubset)) // prints "false"
	fmt.Println(primeNumbers2.IsProperSubset(primeNumbers))      // prints "false"

	// superset

	fmt.Println(primeNumbersSubset.IsSuperset(primeNumbers)) // prints "false"
	fmt.Println(primeNumbers.IsSuperset(primeNumbersSubset)) // prints "true"
	fmt.Println(primeNumbers2.IsSuperset(primeNumbers))      // prints "true"

	// proper superset

	fmt.Println(primeNumbers.IsProperSuperset(primeNumbersSubset)) // prints "true"
	fmt.Println(primeNumbersSubset.IsProperSuperset(primeNumbers)) // prints "false"
	fmt.Println(primeNumbers.IsProperSuperset(primeNumbers2))      // prints "false"

Set Operations

Naturally, the golang-set package provides the typical set operations you would expect, including:

  • Union() – combines all elements from both sets, eliminating duplicates
  • Intersection() – obtains only those elements that exist in both sets
  • Difference() – gets those elements in the first set that aren’t in the second set
  • SymmetricDifference() – the union minus the intersection

Here are a few examples showing these operations in action:

	fibonacciNumbers := mapset.NewSet(0, 1, 2, 3, 5)
	triangularNumbers := mapset.NewSet(1, 3, 6, 10, 15)

	fmt.Println(fibonacciNumbers.Union(triangularNumbers))     // prints "Set{0, 1, 2, 3, 5, 6, 10, 15}"
	fmt.Println(fibonacciNumbers.Intersect(triangularNumbers)) // prints "Set{1, 3}"

	fmt.Println(fibonacciNumbers.Difference(triangularNumbers)) // prints "Set{0, 2, 5}"
	fmt.Println(triangularNumbers.Difference(fibonacciNumbers)) // prints "Set{6, 10, 15}"

	fmt.Println(fibonacciNumbers.SymmetricDifference(triangularNumbers)) // prints "Set{5, 6, 10, 15, 0, 2}"

JSON Functions

golang-set seems to have functions to serialise and deserialise a set to/from JSON. I’m not sure where these would be useful, but I decided to give them a try

Use MarshalJSON() to serialise a set to JSON, which ends up looking just like the slice representation:

	evenNumbers := mapset.NewSet(2, 4, 6, 8, 10)

	jsonBytes, err := evenNumbers.MarshalJSON()
	if err == nil {
		fmt.Println(string(jsonBytes)) // prints "[2,4,6,8,10]"
	}

UnmarshalJSON() is supposed to deserialise JSON back to a set, but it doesn’t seem to work:

	evenNumbers2 := mapset.NewSet[int]()
	err = evenNumbers2.UnmarshalJSON(jsonBytes)
	fmt.Println(evenNumbers2) // prints "Set{}"

I have no idea what’s the problem with this. The JSON functions are neither documented in the readme nor covered by tests, but they were easy enough to discover via Intellisense in Visual Studio Code.

Conclusion

Hopefully this tour of the golang-set package has shown you enough existing Set functionality that you won’t have to write your own set data structure in Go ever again.