Go got generics

March 10, 2022

views
Random

Go 1.18 is (almost) out

The world may be falling apart, but noobs and Leetcode experts alike have at least one reason to rejoice: Go 1.18 is just around the corner.

Along with this greatly anticipated release come generics. If you originally come from a land where generics are a basic feature of the language, you might have been puzzled by the lack of what seemed to you like a fundamental piece of the programming language puzzle.

If you are a total noob, you might not even know that generics exist. And you know what? That's ok, I forgive you.

In this article, we will cover two important historical eras:

  • Prehistory, also known as the era without generics in Go
  • The End of History, also known as the era with generics in Go

At the time of writing this, Go 1.18 is not out yet. You can try it out on The Gotip Playground. You can run all of the code snippets there if you want to follow along.

TL;DR for Giganoobs - Generics what?

Generics let you parameterize functions (and other constructs) by passing them types. Consider a function prototype . You will get a compiler error if you try to pass in a or some other type.

If you want to write an function that accepts any numeric type, then you are going to have to use some form of reflection and pass around or some artificial facade.

Generics let you define a function like where is a generic type parameter and is some form of constraint you define or loot from some other library.

Conclusion: generics good.

Prehistory: no generics

It might be difficult for you, person of the future, to imagine an era without generics in Go. At the time of this writing, this era is unfortunately not over yet.

You probably call this era prehistory. I won't blame you. I too feel like a caveman sometimes, writing such monstrous functions as and . That I live in a cave does not help either.

Let me give you a tour of our era, or at least a tour of my cave. Let me show you how we do things in prehistory.

I'll use the example of adding two numbers together. Keeping it simple for you noobs out there.

Adding numbers

Very cool.

Now let's say that working with is not enough for you and you also want to add together. What do you do?

Here's what you do. You wonder why there are no generics in Go.

You don't stop there though. The next thing you do is start copy-pasting the same function only to change the types.

Copy-paste and change the types

Let's duplicate our logic and come up with beautiful function names!

You may see how this could become an issue if you were dealing with 15 different types that behave similarly. (Although if you have a need for a function that takes in so many different types, I'd expect you could abstract some things away behind legitimate interfaces.)

If you've got your fancy pants, you might even think it would be a great idea to refactor everything to have these functions under packages of their own to avoid names such as or and instead have names like and .

Great idea, that would indeed be a great way of spreading your logic all over the place and turn your code into a giant bucket of spaghetti. Can't blame you though - whatever you can to get rid of style naming.

Casting in and out

Another way would be to cast everything in the calling code. Let's say you force arguments to be and leave it up to the caller to pass in the right type and cast the return value back to whatever type is desired.

At this point, types are just getting in the way more than they are helping. Casting back and forth gets tedious.

Let's look at the next solution then.

interface{}, reflect and type switches

Many of you would probably think of using along with or type switches.

At this point we are getting quite comfortable with the idea that the compiler is just getting in the way and ought to be ignored. Very cool.

Let's write down an implementation of the type switch solution here, just to handle , , , :

This is getting complex just to add numbers together, don't you think?

On top of this complexity, in is now an that you will need to cast back into whatever type it should be.

At this point, we lose almost the entire point of using a language with static types. We can be proud, we have now turned Go into an overly complex Python or JavaScript.

The case for Prehistory

Generics bad

Generics good

While you, cool person from the future, may be horrified by what you just saw in this section, some in our era believe that generics should not be brought to Go.

The main arguments against generics in our prehistoric era seem to be the following - notice how their arguments are not even proper sentences:

  • copy-paste good
  • reflection good
  • generics bloat bad
  • generics complex bad

Well, you make up your own mind, I am not your dad.

Regardless of what any of us may think, the wheels are now in motion. Go 1.18 will be coming in hot within the next couple of weeks.

The End of History: generics

At this point, you should be questioning whether writing any code at all without using generics was even possible back in the prehistoric era.

With that being said, even if you are from the future, you might still be a noob who does not know what generics are. That's ok, you may read on.

Let's see how generics might help you avoid the awkward solutions from the prehistoric era.

Let's recall here that what we were trying to do earlier was add two numbers. We wanted a function that can take in any numeric type, because we only care about the abstract concept of adding things. We do not care so much about what it is that we are adding, so long as what it is can be added.

The naysayers may say: "Define interface addition". You will notice here again that their argument is not even a proper sentence. Regardless of their lack of eloquence, to such naysayers I say "Give me generics or give me death".

Here it is, the end of History, in all its generic glory. The pinnacle of evolution:

Wooow.

Breaking it down

Let's break down what's going on here.

We define an called here. Our is the union of the the , , , types. This interface here will be used as our constraint in our function.

After all, we want our function to be generic, but we do not want it to take in any type at all. For example, our function is not supposed to work if we pass it arguments because it does not make sense to add these.

By constraining our generic type to be , we also guarantee that it will implement the methods of these types, such as additivity with the operator.

Let's look at our function now. It takes in arguments of a generic type (that must be a ) and returns a value of the same type:

What means is define a function parameterized by a type we name and which is constrained to be a (remember the constraint we defined earlier?).

Then means that the function takes in arguments that are of this yet to be known type we just mentioned and that it returns a value of this same type. If you are unfamiliar with Go, you may note that is equivalent to .

The body of the function is , and thanks to our constraint , the compiler knows that is going to be a . Every type in our constraint implements additivity with the operator, so the compiler knows that whatever is, if and are both of type , then must be semantically valid.

You can try function and see what happens. The compiler will tell you that your arguments do not implement (your constraint).

On the other hand, if you had not constrained your type to be of metatype and instead had written ( is an actual type that means any type goes). The compiler would have complained about the line, because it would not be able to guarantee that and implement additivity with the operator.

Calling the function

How do you call this generic function now?

You just call it and pass it your values.

Calling it with :

Calling it with :

VOILA.

Your function is now reusable - pass it your arguments, and it will return a value of the same type.

Wrapping up

This was a toy example involving adding numbers just to give you a sense of how generics can help.

In the real world, you could be dealing with much more complex data structures, more complex interactions and relationships. Slices, maps, whatever else. In more complex cases, you might extract interfaces common to multiple types, but this does not always make sense. In such cases, generics can save the day.

Using generics is cleaner than duplicating functions all over the place only to change the types and rename the function as or . Using generics is also safer and cleaner than passing black boxes around and using type switches, reflection and other unsafe workarounds.

Using generics is also faster than passing, boxing, unboxing and reflecting all over the place.

Anyway. What do you think?