Hero image

Software Development

Generics: What Go Developers Need to Know 

Home

>

Blog

>

Software Development

>

Generics: What Go Developers Need to Know 

Published: 2023/08/17

12 min read

Over the last few years, Golang (Go) developers and enthusiasts have witnessed many new tools, technologies and methods, with generics being one of the most talked about. What are generics, how can they be used and are they needed? Read on to find out. 

What are generics? 

There are many different answers to the question “What are generics”. According to one trusted source, “Generic programming is a style of computer programming in which algorithms are written in terms of types-to-be-specified-later that are then instantiated when needed for specific types provided as parameters”. In other words, generics work in a way where types are not specified at the moment of declaration but are specified according to the usage of the entity. Quite a straightforward concept, isn’t it? 

It’s worth mentioning that generics are not the same as the dynamical typing present in, for example, Python language. These are two different concepts that can’t be mixed. 

But what does it mean for Go? Well, in this context generics are a method of using different types with the same logic, without the need to copy and paste the same function over and over again with only the types changed. A basic example of this usage looks like this: 
func additon[T int | float64](x, y T) T {
return x + y
}
func use() {
additon[int](1, 2)
additon[float64](1.4, 2.6)
}

The code is quite straightforward – there is a declaration of the addition function which accepts two arguments, both with type T, adds them and returns the value, also of type T. About the mentioned type itself, it can be either integer or float64. The use function presents the usage of the described generic function – it makes a call while explicitly saying what type of the arguments will be used. This process is called instantiation.  

Importantly, the compiler itself assures that the types that the function uses will be exactly as expected. No place for understatements here. Among other things, this is one of the differences between generics and the aforementioned dynamical typing.  

But how does Golang do that? To answer that question, let’s explore how generics are implemented in the first place. 

How generics are implemented

To begin, the implementation of generics in Go uses something that can be called partial monomorphization. Oberservers note that monomorphization, as sophisticated as it looks, is a rather straightforward concept. Put simply, when there is a function and that function performs addition on integers as well as on floats, the compiler will replace the generic placeholder used in the source code and produce a copy of this function – one which will handle integers and one for the floats. And that’s it. As a result, the length of compiling time is a bit longer, but a quite efficient polymorphism is achieved. As far as I know, this solution is used for generics’ implementation in C++ and Rust.  

So why does Golang only use it partially? It’s because, instead of working directly on the types of data which would produce a copy of the function for each type, it creates function instantiations based solely on the uniqueness of the GCShape of the arguments. The GCShape of a type is an abstract concept specific to Go and the two concrete types are in the same gcshape grouping if, and only if, they have the same underlying type or are both pointer types. Therefore, the compiler needs to produce the less unique function instantiations which improves its performance in comparison to the previously described “monomorphization”. As a support factor of this approach, the implementation uses a dictionary, which is sent as a parameter to each generic function or method call. The dictionary provides relevant information about the type arguments that enable a single function instantiation to run correctly for many distinct type arguments. This whole process is more formally called the implementation of generics via dictionaries and gcshape stenciling. 

A question which you may be asking yourself now is – how does this implementation affect the performance of the language? 

Basic usage performance 

To answer that, I’ve prepared some basic tests of generics usage. The logic that will be tested is the simple addition in different forms. First, direct implementation for different types:

func addInts(a, b int) int {
return a + b
}
func addFloats(a, b float64) float64 {
return a + b
}
func addStrings(a, b string) string {
return a + b
}

Now, implementation with type assertion together with a switch statement

func addWithTypeAssertionUsingSwitch(a, b any) (any, bool) {
switch a.(type) {
case int:
if b, ok := b.(int); ok {
return a.(int) + b, true
}
case float64:
if b, ok := b.(float64); ok {
return a.(float64) + b, true
}
case string:
if b, ok := b.(string); ok {
return a.(string) + b, true
}
}
return nil, false
}

For the next one, usage of the reflect package: 

func addWithReflection(a, b any) (any, bool) {
  if reflect.TypeOf(a).Kind() == reflect.Int && reflect.TypeOf(b).Kind() == reflect.Int {
    return a.(int) + b.(int), true
  } else if reflect.TypeOf(a).Kind() == reflect.Float64 && reflect.TypeOf(b).Kind() == reflect.Float64 {
    return a.(float64) + b.(float64), true
  } else if reflect.TypeOf(a).Kind() == reflect.String && reflect.TypeOf(b).Kind() == reflect.String {
    return a.(string) + b.(string), true
  }
  return nil, false
}

Last, but not least, here’s generics implementation in two different variants. The first shows the constraints used directly in the function declaration: 

func addUsingGenerics[T int | float64 | string](a, b T) T {
return a + b
}

The second, with the defined type set: 

type possibleType interface {
int | float64 | string
}
func addUsingGenericsWithTypeSet[T possibleType](a, b T) T {
return a + b
}

Don’t worry if you’re confused with some new terms I’ve just used – these will soon be clarified.

The code of the tests itself is a simple example of Go’s built-in benchmark tool. There’s no point into diving into it, although if you’re interested, here’s a link where you can find some information.

What’s interesting are the results:
Go Results
As you might have expected, the slowest approach is the one that uses the reflect package, but let’s leave that out – the drawbacks of this package are not the subject of this article. What’s important to note is that all the other approaches are, more or less, the same. There are some slight differences, but running the tests again and again shows that these results are pretty much interchangeable. So, what does it say about the performance, in the context of generics? The influence of generics on performance is neglectable, as long as performance isn’t your highest priority. And by “highest priority” I understand such circumstances like avoiding interfaces due to performance cost. How often does the average programmer deal with such high-demanding applications? The answer is not often. Therefore, we can assume generics do not
negatively
impact performance in most projects.

How can we use generics? 

Since we know that performance isn’t a blocker for generics usage, let’s look at how we can use them. But before we dive into some examples, let’s clarify some of the new concepts that generics come with.  

These would be Constraints, Sets of types and Type inference. Constraints are just interfaces, which are the type parameters for functions and structs. Sets of types are quite self-explanatory – these are just groups with different types, whether they come with or without any methods. The last one, type inference, describes the possibility of omitting the explicit usage of the type arguments when calling the generic function. The type inference has appeared in the Golang language before, for example, when declaring a variable using “:=” symbols. But in the context of figuring out the function parameter’s type, this is a new thing. 

Let’s see how this aligns together in code: 

package main
import "fmt"
type magicNumber int
// Sets of types
type Number interface {
~int | int64 | int32 | float32 | float64
}
// Constraints used in brackets
func GenericFunc[myType rune | Number](value myType) string {
return fmt.Sprintf("%v", value)
}
func main() {
var x rune = ' '
var y float32 = 1.2
var z magicNumber = 42
a := GenericFunc[rune](x)
// Implicit Type inference examples
b := GenericFunc(y)
c := GenericFunc(z)
/* ... */
}

In this example, two other syntaxrelated additions can be spotted. The first is the “|” sign used in both sets of types and directly in constraints. Basically, it works like a combination of “or” and union. Firstly, the type handled by a given function can be one of that is separated with the “|” sign. Secondly, only the operations which are possible to perform on each of the involved types (such as comparison using operators) will be available on the new combined type. Another new thing is the “~” sign. This token in the generics example indicates the type it is bound to and all the types whose underlying type the mentioned type is. It works despite the number of layers, so it is possible to do something like this: 

type CoolNumber int
type CoolerNumber CoolNumber
type Number interface {
~int | int64 | int32 | float32 | float64
}
func GenericFunc[myType Number](value myType) string {
return fmt.Sprintf("%v", value)
}
func main() {
var x CoolNumber = 1
GenericFunc(x)
}

Before we see some other examples of the generics, lets focus a bit on the Constraintswhat we can use in such role? Can function be a part of the Constraint? Theoretically yes. A function in Go is in a way a regular type so it shouldn’t be a surprise. An example of such a use case could look like this: 

package main
import "fmt"
type ExpectedReturnTypes interface {
uint | bool
}
type ExpectedArgsTypes interface {
int | string
}
func decorator[expectedFunction func(x expectedArg) expectedReturn, expectedReturn ExpectedReturnTypes, expectedArg ExpectedArgsTypes](passedFunction expectedFunction, passedArgument expectedArg) expectedReturn {
magicLogicDecoratorDoes()
return passedFunction(passedArgument)
}
func main() {
f1 := func(x string) uint {
fmt.Printf("¯\\_(%s%s\n", "ツ", x)
return 0
}
f2 := func(x int) bool {
fmt.Println("answer:", x)
return true
}
decorator(f2, 42)
decorator(f1, ")_/¯")
}
func magicLogicDecoratorDoes() {
/*
...
*/
}

We can see here a function (“decorator”) that accepts as an argument another function with two variations of expected arguments and returned values. Is such code useful? It depends. This code is similar to the decorator pattern, at least in its Python flavour. But it doesn’t mean that it’s a good idea to use it. One of the biggest advantages of Go is its readability and with this code it’s hard to say that it is clear and understandable. Nevertheless, it is possible to do something like it. 

Another type that could be useful as a Constraint would be an error type. Unfortunately, it cannot be used as such. If we think about it a bit more, we can see why that is and as well, we can see the overall purpose of generics. 

Forcing a set of methods for an argument is easy since there’s a tool for that – the interfaces. This is their purpose. However, the change that came with implementing generics means that it’s possible to extend the usage of interfaces from a set of required methods to the set of possible types. The interface which is focused on types is called Set of types. This difference – putting more pressure on the types – that’s the core of generics. And that’s the reason why the “error type” is not allowed in the union with any other type in Set of types. It’s because, beneath the “error type” is just an interface. And the interfaces cannot be used in union in Set of types. 

That said, there is another way ordinary interfaces can be linked with the Set of types. These two can be combined. Here is an example of such a merger: 

package main
import "fmt"
type XI interface {
Do() string
~int | ~string
}
type X string
func (x X) Do() string {
return string(x)
}
type Y int
func (y Y) Do() string {
return fmt.Sprint(y)
}
func genericFunction[T XI](arg1, arg2 T) string {
if arg1 <= arg2 {
return arg1.Do()
} else {
return arg2.Do()
}
}

Interface XI has one defined method and also it is a Set of types. Due to the latter, in the function it must be used as a constraint. In use, it has the Do method available, which is the same as it would be with the ordinary interface. The benefit that comes from the fact that it is also the Set of types is a possibility to use not only the methods, but also the operators on the arguments that have this type. This gives more flexibility on working with arguments, especially if beneath those are basic types that need some methods on them. 

Finishing the constraints concept, we can summarize it with the following points: 

  • Anything can be a constraint (simple types, structs, functions, interfaces) 
  • Not everything can be in a union in constraint (interfaces cannot be) 
  • Putting functions into constraints is possible, but it’s not the best idea due to the readability of the code 
  • The argument within the constraint types will be able to do all operations that are common for all the constraints (or parts of the Set of types).  
  • There is a possibility to combine Set of type’s behavior with regular interfaces’ behavior 
  • If you need to assure only the methods from the passed argument use interface rather than generics 

Up until now we were discussing usage of generics only within the functions. But what about the structs and their methods? Of course, those also can utilize generics features, although the syntax, especially at first sight, may be a bit cumbersome

package main
type User struct {
Name string
}
type Team struct {
ID int
}
type GenericResponse[T any] struct {
dbEntity T
}
func (gr GenericResponse[T]) Entity() T {
return gr.dbEntity
}
func queryDB[T any]() []GenericResponse[T] {
/*
...
*/
}
func main() {
users := queryDB[User]()
teams := queryDB[Team]()
/*
...
*/
}

As we can see, the constraints in this case must be present in the struct definition. All methods also must be defined alongside the square brackets, but there, these are used as a part of instantiation. Additionally, methods must have no type parameters, therefore can’t be generic itself. 

Why do we need generics? 

To answer that question, let’s start with the Golang community’s opinion. A 2020 survey reveals the following results: 

Go Survey

According to this, generics were the most desired functionality. So, we know that, as a community, Go developers wanted generics. But this doesn’t answer the question: do we need them?

To answer that, we need to look at the reasons why one would like to use generics in their code. The first reason is to handle functions that share the same logic among many simple types. Arithmetics is a basic example, but there can be more. Secondly, generics help developers handle functions that work on slices, maps and channels of any type. For example, a function which would read from every channel and return a slice with all the received elements. A third reason to use generics in code is connected to general purpose data containers. These are pretty useful and can significantly reduce the amount of code through the use of structs, whose main purpose is to structurize the data, not to provide any methods or logic on it. 

Generics alternatives 

Most of the described use cases can be done without generics. Sometimes using the any type as an argument’s type, sometimes using the type assertion with or without a switch statement. But these options don’t come without problems. None of these methods give off the confidence that everything is okay on the compile time. Type assertion can even cause a service to panic if implemented improperly. Of course, there is always another alternative, the most straightforward – just write more code for different types. Unfortunately, there is a cost to that. The more code, the easier it is to introduce a bug. As well, maintenance becomes more difficult. While there are possibilities to generate that kind of code, I don’t think this is the solution for the issues I’ve mentioned. Also, code generation introduces its own problems, which will not be described here.  

All of this doesn’t mean that generics comes without any drawbacks. For one thing, there’s the new syntax, which is, in a word, different. Maybe it’s because the feature is still new, but in my view, it disrupts the readability of the code, at least for now. Also, I think that using generics, even with the purest intentions, may easily make code really unreadable. Additionally, the adoption of generics in the built-in libraries doesn’t seem like to be happening. Maybe there have been some initiatives, but over a year after the release of the Go 1.18 e.g. in math package, there are still no generic features. What can we conclude?  

In my opinion, it’s like when channels were added. For now, we, as the Go community, don’t yet know when to use them. We’ve got a new toy and we’re trying to figure it out. During this period, we’ll probably use them incorrectly. But, at some point, sooner than later, we will define some patterns and good practices for the language that will naturally utilize the feature. Are generics really that important thing for Golang? I don’t think so. I’m pretty sure that many of us will write good, quality code for a year and will not use generics even once. But it doesn’t mean this feature is useless. In the proper context, it can be a real asset that will save time and enable us to write way less code. And that’s one thing I wish for all of us – the ability to recognize these opportunities.  

If you’re interested in hearing how our experts can ramp up your capacity and increase your capabilities, get in touch by filling out the contact form.

About the authorMarcin Plata

Senior Software Engineer

A backend developer with 4 years of experience, Marcin first started programming in Python, but eventually fell in love with Go. In his daily work as a senior software engineer, Marcin creates and maintains microservices that work in systems based on event-driven architecture. An avid tech enthusiast and Go disciple, Marcin enjoys keeping up to date with the latest programming developments and sharing new knowledge with colleagues and clients.

Subscribe to our newsletter

Sign up for our newsletter

Most popular posts