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.

Leave a Reply

Your email address will not be published. Required fields are marked *