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 elementsIsSubset()
returns true if the first set is a subset of the second set, or if they’re equalIsProperSubset()
returns true if the first set is a subset of the second set and they aren’t equalIsSuperset()
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 equalIsProperSuperset()
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 duplicatesIntersection()
– obtains only those elements that exist in both setsDifference()
– gets those elements in the first set that aren’t in the second setSymmetricDifference(
) – 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.