CodeX
Published in

CodeX

Using generics in Go

Generics were introduced in Go a few months ago with the release of Go 1.18. I am fairly new to Go, but I am accustomed to other languages that already make use of generics for a while, like Java or Kotlin.
With the release, I noticed that the community was a bit apprehensive about the new language feature and on the defensive about its usefulness.

In this article, I’ll talk a bit about generics and show a possible and plausible usage of it in Go, and try to clarify how it can be helpful in your projects.

Complaints with generics

The questions/comments I commonly see around forums and general discussions on generics are

“Why do we need generics?”
“I can do everything I need without Generics.”
“it makes code more difficult to read.”

It is true that you don’t need generics, the same way you don’t need a smart IDE… but it sure is good to have it, isn’t it?

Yes, you can do everything without generics. Generics are not here to allow you to perform operations that you could not do before… generics are here to allow you to perform those operations with less code. That’s the whole thing, at least the way I see it, about generics: it enables us to be more succinct, doing the same but without repeating ourselves.

Finally, it is difficult to read code that makes use of generics at first, because you’re not used to it. The same way it was difficult to understand code when you first started coding, or even the same way it is difficult today when you need to change languages. Generics make things a bit different, and you just need to get used to it. Either way, code that makes in-depth use of generics usually gets sunken into underlying definitions… so you won’t see it very often.

“Is that really true?”

Maybe you didn’t realize but, even if you are in a Go version before 1.18, you are already making use of generics. Slices and Maps are both structures that already “make use” of generics even before it was introduced for general use… and you were using those without seeing generic code. 😉

Generics use-cases

The typical use case for generics, from my experience, is to make certain structures — like collections — and functions usable for several types, instead of being tied to specific types defined in the signature.

For instance, you can declare several different maps like this:

mapA := make(map[string]int)
mapB := make(map[int]string)
mapC := make(map[int]float32)

You were able to declare three different maps in three lines: they are ready to use.
The code that defines how a map should work is the same. All three maps work the same way. The only thing that changes are the type parameters for each map.

Because a map behaves the same whatever the types involved are, the map definition could be made generic. This leverages us to use the same definition and instantiate it three times, instead of having to code three different implementations of the map collection and then instantiate each one.

So… you get the drill: If the behaviour is constant, it can be generic.

Making use of generics

Now, into what I really wanted to show you: Writing generic code in Go.
When I started using Go, I felt almost immediately the void of not having a Set implementation.
I went to search for the reason why, and it was clear once I found it: A Set is basically just a map in which we only really care about the keys.

This was enough for me to accept it. That being said, it always bugged me a bit that, for instance: Every.Single.Time I wanted to filter duplicates out of a list, I had to do something like

theList := <list with loads of duplicates>

set := map[string]bool
for _, entry := range theList {
set[entry] = true
}

theList = nil

for key := range set {
theList = append(theList, key)
}

....

Instead of what I was used to: a much less verbose way to write the same thing

var myList = <list with loads of duplicates>
var noMoreDups = myList.toSet().toList()

But okay, I had to live with it. After all, I could not make my own Set implementation with functions enclosing all that Go code. I mean, I could…but it would be tightly coupled to specific types.

“Lies! You could use any or interface{} as the key type of the map, it would work just fine!“

Not really… at a minimum, you’d lose all context of what type of data you were handling as soon as the list values were dropped inside the ‘improvised set’. Using generics, that does not happen. You keep your types.

I get the feeling you see where this is going right? I ended up implementing the type Set, making use of generics, with all the goodies I was used to.

I will just leave you the interface declaration as the whole code would be awkward to have here as a huge block of code. You can still find the complete implementation though: on my GitHub repo where I have it stored.

type ISet[T comparable] interface {
Contains(value T) bool
ContainsAll(values []T) bool
Add(value T)
AddAll(values []T)
GetSize() int
Remove(value T)
RemoveAll(values []T)
ToSlice() []T
ExtractMapPrimitive() map[T]bool
}

As you can see, I’ve also added a function to make it possible to easily go back to the old ways of map[Something]bool…traditions are meant to be respected after all. 🤪

The usage of this Set type in your everyday Go code feels pretty natural:

 ourSet := NewSetOfType[string]()
stringA := "a"
stringB := "b"
stringC := "c"

sliceWithDups := []string{stringA, stringB, stringB, stringC, stringC}

ourSet.AddAll(sliceWithDups)

fmt.Println("The Slice with Duplicates")
fmt.Println(sliceWithDups)

fmt.Println("The set when converted back to slice without duplicates")
fmt.Println(ourSet.ToSlice())

If you run this code you’ll get:

The Slice with Duplicates
[a b b c c]
The set when converted back to slice without duplicates
[a b c]

Our code to filter out duplicates from a list would look much better now:

//BEFORE

set := map[string]bool
for _, entry := range theList {
set[entry] = true
}

theList = nil

for key := range set {
theList = append(theList, key)
}
//AFTER

ourSet := NewSetOfType[string]()
ourSet.AddAll(sliceWithDups)
theList = ourSet.ToSlice()

To each their own but, for me, it is much more approachable now.

Adding to the readability improvement, you can do this with whatever types you want — again, the implementation is generic:

 stringSet := NewSetOfType[string]()
intSet := NewSetOfType[int]()
floatSet := NewSetOfType[float32]()
uuidSet := NewSetOfType[uuid.UUID]()

I’ll also leave you with access to some test code for you to run… in case you are still sceptic about the validity of this whole thing. 😛

Conclusion

In this article, I wanted to write a little bit about what I like about generics, where I find the feature to be useful and the sort of things it makes possible.
For me, it is a great tool on the DRY front, and it also allows you to make the code containing your business logic much more readable and less cluttered.
I hope you were able to see my angle on this subject and perhaps also realize the value it brings to Go as a language.

If not… it is also fine to not like generics or to not use them in your projects. 😄

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store