Content

Terminolog ... Operations ... Generics w ... A note on ... Generics w ... constraint ... Conclusion
Avatar image of Mario Škrlec

Go generics

Go in version 1.18 adds support for generics. In this post, we will learn generics with some (very) simple examples.

Go generics

Image provided by Unsplash from Visax . Download the image and support the author!

Generics are a way of writing code that does not need to specify the exact type of values but you can work with them as if they have a certain type. No explanation or definition is good enough, but we will be writing some simple examples to make it easier.  

While I write this blog post, I will also be learning generics. If you see any mistakes or you see something that I misunderstood, write a comment and I will correct it. 

Before version 1.18, we used the interface{} type that can hold any type but must be converted into that type if we wish to use it. 

The code above, if you uncomment the commented lines, will fail with error intVal declared but not used. That is because converting myValue to int is just fine at compile time. Only at runtime, will this program fail because we tried to convert string that interface{} holds into an int. Let's see how generics could solve this problem. 

We created a returnType() function that returns the value the same as the argument it is passed. But, with generics, the type is checked at compile type and there is no chance of a runtime error. Of course, this example is extremely simple. But from this example, we can find out something interesting. 

Try running this code to see the error it produces. The reason for this error is that, when you call returnType[string] (or returnType[int]) with some argument, that argument can only be one of those two types. If we simply return a string constant, that value will always be returned which is not allowed even though our type K supports the string type. This tells us that the Go compiler will be able to shield us from unintentional return types at compile time. 

Terminology

A generic function consists of type parameters and type constraints. From the official Go blog:

Type parameters make the function generic, enabling it to work with arguments of different types. You’ll call the function with type arguments and ordinary function arguments

In our simple example from above, K is a type parameter while int | string is a type constraint. Calling code can only be called with int or string type parameters.

Operations on generic types

Our last example was very simple and we did not do anything with the arguments. Let's correct that.

We created the Adder interface that implements some number types. We left out most of them for brevity. This code will compile and run successfully since all types implemented support adding with the + arithmetic operator. But this operator is also used for string concatenation. Let's add the string type to our previous example. 

The above code works just fine. The reason why we can do this is that the compiler knows that the Adder interface is constructed from types that work with the + operator. If we change the operator to -, the program will fail at compile time since you cannot subtract strings. Try to edit the above code so that it substracts a and b variables and see what happens.

Comparable operators are also supported. If you try to compare a and b with <, ≤, == etc… this too will work since all the underlying types of Adder interface support these operations. In fact, Go SDK has an interface for that purpose. It's called comparable and we use it as any other type. 

Generics with maps and slices

Working with maps and slices is unavoidable in Go. Generics can also be used with maps and slices. The most basic example for maps and slices is searching for an element. Without generics, we would have to create a new function for every type that we are searching. For example, if you wish to search for a specific string in a slice of string, you would have to do something like this:

contains function is pretty common and can be used on many types. There is nothing type specific in it to not be used with ints or floats or any other type that supports == operator or looping. But, if we wanted to create this function to search for ints, we would have to create a new function and copy/paste the code inside contains. This is where generics come into play. Only one function is needed. 

Maps are different. They require two parameters; one for key and one for value. key parameter must be comparable but value can be any type.

You may have noticed that I used any as the parameter type. any type is just an alias for the interface{} type. The code above would run just fine if you replaced any with interface{}.

What about map values? The same function, albeit modified could be used to create a function that returns values. But, let's go a step further. Why not create a function that will return both keys and values of a map?

Without generics, we would have to either create a function for every key and value type or do some magic with the interface{} type, which as we know, is evaluated at runtime, thus very unsafe. 

A note on instantiation

When you call a generic function, you actually instantiate it. You create a new function with the type parameter baked in that can be reused throughout your code but only with that specific instantiated type. 

Generics with struct types

struct types follow the same principles and rules as regular functions with a few differences. Let's start with the basics

If you instantiated the root node with a specific type, all child nodes must be of this type. In our example, we created Node[string], therefore, its child nodes (left and right) must be instantiated with string type. Try running the below code to see the error. 

We mentioned instantiation. As per the spec, instantiation replaces generic types with its type arguments. In the example below, we are instantiating the Node struct with the string generic type. When go compiles our program, it substitutes T with string, therefore every time you use this type, it must be working with strings. If you wish to work with some other type, you have to instantiate Node struct with this type. 

constraints package

Up until now, we haven't mentioned the constraints package. This package is not part of the Go SDK and it must be installed separately. The reason why I'm mentioning it only now is because this platform cannot (yet) install external packages and, it really isn't that hard to figure out. Below, you can see the full source code of this package. 

You can use this package when you don't want to list all the possible number types go can offer, but instead, use constraints.Ordered as a generic type. It's that simple. 

Conclusion

Unlike in other languages like Java or C#, Go's implementation of generics is simple but powerful enough that we can do big things with it. I hope you enjoyed this article and learned something. Feel free to share your thoughts in the comments.

Avatar image of Mario Škrlec
Like
Tweet
Share
Copy link

Hello, visitor.

This blogging platform is created specifically for software developers. We aim to support many more programming languages and development environments but for that, we need your support. If you like this blogging platform, consider using it to write your blogs.

We tried to make your experience of creating blog as painless as possible soSign in and give it a try.

Guide

RebelSource 2022. All rights reserved.