Represent and transform data safely

How to Use Sets in Go with mapset and Map-Based Sets

Build sets in Go with map[T]struct{} or mapset v2, including membership, deduplication, union, difference, and concurrency tradeoffs.

By Niko Minadze5 min read

Editorial illustration for How to Use Sets in Go with mapset and Map-Based Sets

Go has no built-in set type. For simple uniqueness and membership checks, use a map whose keys are the set elements. When you also need operations such as union, intersection, difference, or subset checks, github.com/deckarep/golang-set/v2—usually imported as mapset—provides a typed set API.

A set is an unordered collection of unique elements. Adding the same element twice does not create two entries, and membership is about whether an element exists rather than where it appears.

Build a set with map[T]struct{}

Go maps already enforce unique keys, so a map can represent a set directly:

map[string]struct{}

The keys are the elements. The values carry no information and exist only because every map type needs a value type.

The empty struct, struct{}, is preferable to bool here. It occupies zero bytes and makes the map's intent clear: only the keys matter. A Boolean value also creates an avoidable ambiguity because a present key can store false, making a one-value lookup indistinguishable from an absent key.

Here is a complete example:

package main

import "fmt"

func main() {
	set := make(map[string]struct{})

	// Add an element.
	set["example.com"] = struct{}{}
	set["go.dev"] = struct{}{}

	// Check membership.
	if _, ok := set["example.com"]; ok {
		fmt.Println("example.com is present")
	}

	// Remove an element.
	delete(set, "example.com")

	// Iterate over the remaining elements.
	for domain := range set {
		fmt.Println(domain)
	}
}

The two-value lookup is important:

_, ok := set[key]

A map lookup returns the value's zero value when the key is missing. Because the zero value of struct{} is also struct{}{}, inspecting the first result cannot establish membership. The ok result is true only when the key exists.

Map keys must be comparable. Strings, integers, booleans, pointers, arrays, and comparable structs can be keys. Functions, maps, and slices cannot.

For example, this struct can be used in either a map-based set or a typed mapset because both fields are comparable:

type Person struct {
	name string
	age  uint8
}

people := make(map[Person]struct{})
people[Person{name: "Mira", age: 31}] = struct{}{}

Where a plain map starts to get repetitive

A native map is often all you need for deduplication or a membership filter. More involved set algebra requires writing loops: union copies keys from both inputs, intersection checks every candidate against another map, and difference retains keys missing from the other map. Subset and superset checks require similar traversal.

Those loops are not inherently problematic, but they become repeated application code. The mapset package supplies these operations behind a common set interface, along with non-threadsafe and threadsafe implementations.

Install mapset v2

The generic v2 package requires Go 1.18 or later. Install it with:

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

Import it using the conventional alias:

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

The type parameter must be comparable. For example, you can create integer, string, or struct sets:

integers := mapset.NewSet[int]()
words := mapset.NewSet[string]()
people := mapset.NewSet[Person]()

_, _, _ = integers, words, people

Add elements and check membership

Add inserts an element, while Contains reports whether it belongs to the set. Duplicate additions are automatically deduplicated:

package main

import (
	"fmt"

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

func main() {
	services := mapset.NewSet[string]()

	services.Add("accounts")
	services.Add("billing")
	services.Add("accounts") // Duplicate: still only two unique elements.

	fmt.Println(services.Cardinality())       // 2
	fmt.Println(services.Contains("billing")) // true
	fmt.Println(services.Contains("search"))  // false
}

Unlike map[T]struct{}, this API does not require the comma-ok idiom: Contains directly returns the membership result.

Comparable structs work the same way:

type Person struct {
	name string
	age  uint8
}

people := mapset.NewSet[Person]()
people.Add(Person{name: "Mira", age: 31})
people.Add(Person{name: "Mira", age: 31}) // Same element, not a second entry.

Chain unions and calculate differences

Union returns all unique elements from both sets. Because it returns another set, calls can be chained across several inputs. Difference returns elements in the receiver that are not in the argument.

frontend := mapset.NewSet[string]()
frontend.Add("html")
frontend.Add("go")

backend := mapset.NewSet[string]()
backend.Add("go")
backend.Add("sql")

operations := mapset.NewSet[string]()
operations.Add("go")
operations.Add("linux")

all := frontend.
	Union(backend).
	Union(operations)

frontendOnly := frontend.Difference(backend)

fmt.Println(all.Cardinality())                 // 4
fmt.Println(frontendOnly.Contains("html"))      // true
fmt.Println(frontendOnly.Contains("go"))        // false

The chained union contains html, go, sql, and linux; repeated occurrences of go remain one element. The difference contains html because it appears in frontend but not backend.

The package also exposes intersection and subset/superset operations, avoiding the custom traversal those operations require with raw maps.

Choose based on the operations and ownership

Use map[T]struct{} when the set is local, membership and uniqueness are the main requirements, and a few explicit loops would remain easy to understand. It has no dependency and uses familiar language constructs.

Use mapset when set algebra is central to the code: Union, Intersect, Difference, and subset or superset checks communicate intent more directly than repeated map loops.

For implementation choice, prefer the package's non-threadsafe implementation when the set is confined to one goroutine and performance is the priority. Choose the threadsafe implementation when the same set must be used concurrently by multiple goroutines. Thread safety has a purpose; it is not a decorative setting to enable automatically.

Comments

No comments yet.

Leave a comment

Your comment appears after approval. Plain text only; links stay as text. Line breaks and indentation are kept.

Up to 5,000 characters. No account or email needed.

All articles

Type at least two characters.

Press <kbd>Esc</kbd> to closeOpen the search page