Dwi Wahyudi

Senior Software Engineer (Ruby, Golang, Java)


In this post, let’s talk about using Go 1.18 generics feature to create a set data structure.

Overview

Set is just a list but with unique members/elements, which means we would’nt find any duplicate in it.

Before Go 1.18, without generics, we need to create different sets of methods for each data type. Set methods for string, set methods for int64, set methods for float64, and so on.

With generics now, it’s very simple, because we can parameterized the type.

Set Type Declaration

First, we’re going to declare the Set data type, here we create comparable. Let’s make a new package set.

package set

type set[T comparable] map[T]bool

Here we’re using map data type for storing our set members. Rather than slice, map is better since it is more performant when searching for the element.

What’s this comparable? It’s predeclared identifier.

tl;dr Any data type that can operate on comparison operators (==, !=, <, >, etc) will work just fine. Structs comparison will compare all fields of it.

Set Initializer

Now, let’s setup our set initializer function, we want to receive any comparable type, so here we go.

func New[T comparable]() set[T] {
	return make(set[T])
}

This method will return an empty map with specified data type.

Add Method

Now let’s create Add method for that Set type.

func (s set[T]) Add(values ...T) {
	for _, value := range values {
		s[value] = true
	}
}

Map data type will keep its key uniqueness. So adding duplicate data will ignore the duplicates.

IsMember Method

After Add method above, we would like to create a method to check whether a value is member of the set.

func (s set[T]) IsMember(value T) bool {
	_, ok := s[value]
	return ok
}

Delete Method

Delete method will remove a member from the set, return false if such member doesn’t exist.

func (s set[T]) Delete(value T) bool {
	if s.IsMember(value) {
		delete(s, value)
		return true
	} else {
		return false
	}
}

Len Method

This method can be used to check the length of the set.

func (s set[T]) Len() int {
	return len(s)
}

Members Method

And finally Members method will return all of set’s members.


func (s set[T]) Members() []T {
	members := make([]T, 0)
	for member := range s {
		members = append(members, member)
	}

	return members
}

Testing The Set Method

Let’s now make a unit test for that code, but in order to test a set of structs, let’s create a new package entity.

package entity

type Song struct {
	Title       string
	Artist      string
	ReleaseYear int16
}

And here’s the test functions:

package set_test

import (
	"go-test-set/entity"
	"go-test-set/set"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestInteger(t *testing.T) {
	intSet := set.New[int]()
	intSet.Add(4, 6, 10, 12, 18, 12)
	intSet.Delete(10)

	assert.Equal(t, intSet.Len(), 4)
	assert.True(t, intSet.IsMember(4))
	assert.ElementsMatch(t, intSet.Members(), []int{4, 6, 12, 18})
}

func TestString(t *testing.T) {
	stringSet := set.New[string]()
	stringSet.Add("burger", "softdrink", "pizza", "soto", "burger", "rendang")
	stringSet.Delete("ice cream")

	assert.Equal(t, stringSet.Len(), 5)
	assert.False(t, stringSet.IsMember("kebab"))
	assert.ElementsMatch(t, stringSet.Members(), []string{"burger", "softdrink", "pizza", "soto", "rendang"})
}

func TestStruct(t *testing.T) {
	songSet := set.New[entity.Song]()
	songSet.Add(entity.Song{Title: "Stayin' Alive", Artist: "Bee Gees", ReleaseYear: 1979})
	songSet.Add(entity.Song{Title: "Dancing Queen", Artist: "ABBA", ReleaseYear: 1976})
	songSet.Add(entity.Song{Title: "Blitzkrieg Bop", Artist: "Ramones", ReleaseYear: 1976})
	songSet.Add(entity.Song{Title: "Dancing Queen", Artist: "ABBA", ReleaseYear: 1976}) // will be ignored

	assert.Equal(t, songSet.Len(), 3)
	assert.True(t, songSet.IsMember(entity.Song{Title: "Blitzkrieg Bop", Artist: "Ramones", ReleaseYear: 1976}))
	assert.ElementsMatch(t, songSet.Members(), []entity.Song{
		{Title: "Stayin' Alive", Artist: "Bee Gees", ReleaseYear: 1979},
		{Title: "Dancing Queen", Artist: "ABBA", ReleaseYear: 1976},
		{Title: "Blitzkrieg Bop", Artist: "Ramones", ReleaseYear: 1976},
	})
}

With this unit test, we can verify that the set methods above with generics can receive any comparable data type (including struct which will be compared field by field).

comparable in Go has limitations, it’s not interface, we cannot implement it, so we can’t compare by specific field. If we want to sort a slice of structs based on a specific field, for now sort package is the way to go, https://pkg.go.dev/sort.