Updated: Jun 3
Generics is one of the most anticipated and long awaited features in go. Some also argue that it is in some ways a controversial feature since it seems to go against one of the go language's core design principle "simplicity". This however is a topic of discussion for another day, in this article we will go through everything that you need to get up and running with go generics. Also we will delve into some of the finer details and best practices of go generics to get you an advanced level knowledge on this topic.
The go generics is finally here! The generics feature was added to the language in the Go release, version 1.18.
What is generics in a programming language?
Generics is a programming language paradigm that gives us a way to write code that is not tied to any specific type. It gives us the ability to define a generic or common data structure/function that allows us to work with multiple data types (like int, float, string etc).
Why generics is needed?
Let us understand this with an example. Assume we have a function Add(), that adds two integer types and returns the result as an integer as shown below:
The above function works fine as long as our use case is only to add two integer values. Suppose, tomorrow we have a new requirement where in we are required to support float type addition as well, how can we handle this? We cannot use our earlier function because it takes only integer types as input.
Prior to Go generics this could be solved in one of the two ways:
Defining multiple functions, one for each type.
Using an empty interface and type asserting.
A natural tendency to solve this is to define a new function that does the exact same thing as our earlier Add() function but with float64 type as shown below.
As you can see this is unnecessary duplication of code. It may not seem like a big deal for the above example as our function only involves a simple logic to add two numbers. But in the real world we may have to deal with a much more complicated logic containing hundreds of lines of code and duplicating these complex functions is a waste of time and effort. Also this introduces a maintenance overhead because every time we need to improve or update some piece of code we would have to do this in all the duplicated blocks, which of course is not the best way to handle this.
In this approach we use an empty interface that can accept values of any type and in the function body we use type assertion to extract the required type and perform necessary actions.
While this looks cleaner than the first approach, it still involves a lot of boilerplate code and is not the most efficient solution to our problem. Scenarios like these is exactly where generics comes into play.
The generics feature in Go is a major release, according to the official documentation this is the biggest change made to the language since the first open source release. The good news however is that it is fully backward compatible with the code written using earlier versions of Go.
In Go a generic type is generally denoted using the notation T, however we are not restricted to using that, we can name it anything.
Fig.1 shows a sample Go generic function along with its components. Compared to a normal (non-generic) Go function, you can see there is an additional square bracket between the function name and the parameter list. Also the parameter list contains the generic type parameters (denoted by T).
Go generics can be broadly broken down into 3 components:
Type sets or type constraints
Lets discuss each of these components in detail.
Want to master coding? Looking to learn new skills and crack interviews? We recommend you to explore these tailor made courses:
In Fig. 1, the square brackets and the elements inside it together is called a type parameter or type parameter list. Type parameter defines information about the generic type. It contains information like the name of the generic type, data types supported by the generic type etc.
A type parameter is defined using the syntax:
[T1 constraints, T2 constraints, ...]
Here are a few type parameter definition examples:
[T int | int32 | int64]
[T1 int32 | float64, T2 string | float64]
Note: constraints.Ordered is a type of constraint provided by Go and it is defined in the package golang.org/x/exp/constraints, it supports Integer, Float and string types (More details on the constraints.Ordered type can be found here).
Type parameters can be applied to Go functions and Go types (go type keyword).
1. Type Parameter on Go Functions
The sample function shown in Fig. 1 is an example of how a type parameter can be applied to a Go function.
Let us understand this more clearly by studying how we can redefine our earlier Add() function to support integer and float types using Go generics.
As you can see we can convert our non-generic Add() function to a generic Add() function by adding a type parameter definition after the function name and replacing the specific types (int, float64) with generic type T.
This generic Add() function can be called using both integer and float data types, there is no need to redefine or duplicate the function body like we saw earlier.
How to call a generic function?
Calling a generic function involves two steps: 1. Instantiation 2. Function call
In this step we tell the compiler what specific type we want to pass into our generic type. The compiler then checks whether this data type satisfies the type parameter constraints. For example the constraints, constraints.Integer and constraints.Float types used in our above generic Add() function, supports Integer, Float data types. If anything other than these types is used during instantiation, it throws a compile time error.
The syntax for instantiation is:
funcVariable := function_name[data_type]
For example we can instantiate our above generic add function with int data type as shown below:
AddFunc := Add[int]
For float64 type we need to use float64 inside the square brackets as shown below:
AddFunc := Add[float64]
The instantiation step returns a func type variable. In this step we call the generic function using this func type variable that we obtained during the instantiation step as shown below:
result := AddFunc(10, 20)
So to summarize, in order to call a generic function we need to first instantiate and then call the function as shown below:
AddFunc := Add[int] result := AddFunc(10, 20)
Go also supports a simplified syntax where we can combine the instantiation step and function call step into a single line of code:
result := function_name[data_type](argument_list)
This means we can call our Add() function using a single line of code as shown below:
result := Add[int](10, 20)
2. Type Parameter on Go Types
Type parameters can also be applied to types defined using the Go type keyword. Let us understand this again by taking the addition example:
Let us define a custom add type (a generic type) using type parameters. The custom add type struct should have two fields for storing the numbers to be added.
The Add() function should add the values in these two fields and return the result.
In above example we have defined a custom struct type CustomAddType that has two fields num1 and num2. Both num1 and num2 are of type T (generic type). The type parameter is defined after the type name inside square brackets.
We have defined an Add() method for this generic struct type. This method adds the generic types num1 and num2 and returns the result.
To call this add method we need to instantiate the CustomAddType type first and then call the Add() method on it as shown below:
Since num1 and num2 are generic types we can pass both int and float (defined by type constraints) values to it.
In the previous section we have learnt that type parameters can be defined with the syntax:
The "constraints" part in the type parameter syntax is what we refer to as the type sets or type constraints. In simple words type sets define the range of types or set of types a generic type T supports.
The benefit of using type constraint is that it allows us to define a set of supported types. This approach is unlike the generics implementation in other languages like C#, C++ where type parameters are completely type agnostic. The type constraint way of implementation is intentionally added to Go generics to reduce misuse.
Type Sets are Interfaces
An important thing to note is, everything that we define as a type set is an interface! Yes that's right, every type set is an interface. For example the constraints.Ordered type set we saw earlier, is an interface defined in constraints package. The definition of constraints.Ordered is as shown below:
Similarly, constraints.Integer and constraints.Float types that we used in our generic Add() function are also interfaces.
New Interface Syntax
If you have been using Go for a while now, the interface syntax you see above might look a bit weird to you. Interfaces in Go used to have only methods and other interfaces embedded in them, but this is a little different. That's because, this is a new syntax introduced in Go 1.18 for use in generics. Now we are allowed to have types inside interfaces as well. We are also allowed to specify multiple types inside interfaces separated by the union operator as shown in the example below:
The MyInteger interface shown above defines a new type set with int, int8 and int16 as possible types. The | symbol denotes a union, meaning the MyInteger interface is a union of int, int8 and int16 types.