Generics in Go 1.18
Generics is the new functionality added to the Go language in version 1.18. In this article, we will see the syntax, the use cases, and their limitations. At last, we will conclude if it is ok to use generics in production?
What’s New?
The primary use case of generics is to define a Type Parameter that replaces the type. The Type Parameter is valid when implementing the same logic for different types. Type Parameters are defined in square brackets [] after the name of a struct, function, or receiver.
In the following example, P that has green color is a Type Parameter and the int | float64 | int16 that has yellow color is the element union of that Type Parameter that constraint it to those types.
Type Parameter
Traditionally if we want to write a Min function, we would implement the logic for both int and float64 or any other type we want.
Now, we can define a Type Parameter T which can be int or float.
for calling generic functions, you have two options; call them explicitly by passing the Type Parameter or let the compiler decide the Type Parameter based on the input parameters.
If you need more Type Parameters, you can separate them by a comma.
Calling them is the same as a single Type Parameter function. pay attention that here x and y can have different types. for example one of them can be int and another be float64. but in the previous example, both inputs had to have the same type.
Interface Constraint
What we did before is fine if we have a small number of Type Parameters. If the number of Type Parameters is higher than two or the element unions are long functions become long and ugly. Interface constraint is here to rescue us. It is an interface including all the types we want.
For instance, in the following example, we defined an interface constraint called Number, which includes types int and float64. Then, we can use the Number instead of the element union whenever we want the Type Parameter to be either int or float64.
Calling them is the same as before but the function signature is much more compact and readable.
Base Type Parameter
What if we have a type whose underlying type is int? Would that work with the Number too? The answer is no; by default, this behavior is not allowed. ~ Tilda is the new keyword that will enable us to support defined types. By putting ~ before types, all the types with an underlying type of int or float64 are accepted in this function too.
Here we defined AnotherInt with the underlying type of int, and we can pass it to the IsEqual function.
A More Complex Example
It is common to call a function on inputs. For a generic input parameter, you have to declare functions explicitly in the interface constraint to be able to call the functions.
Here we defined an interface constraint Stringer which explicitly declares String() function for both type Age and Name.
Calling them is the same as before.
Any Keyword
Any is my favorite change since I thought an empty interface was ugly to use. The any is an alternative for interface{}.
Here we leveraged any keyword to accept any input.
because any is ok with any type, we can pass anything.
Comparable
It is a new keyword for comparing objects and is applicable for types that implement equal and not equal functionalities.
Here we check if two arrays are equal. We set T as comparable because we want to compare array elements and nothing else.
you can pass any comparable type to this function; in this example, the type is int.
Generic Struct
Now, we can define generic structs too, which gives us great power in implementing many things, including data structures.
Here we define a list struct that embeds a slice of items. It has the following functionalities:
- Size(): return size of items
- Add(T): add an item to the slice
- Foreach(func(int,T)): iterate through items and call input function on them.
for creating an instance of the generic struct, we explicitly declare the type parameter before the struct name.
Here we created a box of integer numbers. Then we printed the size of the box, added an element to it, and foreach through items to print them.
Because there was only a slice in the Box struct, we can define the Box struct this way too.
Generic Channel
We can also define generic channels. Here, we defined a function that accepts two generic channels and merges them in a mediator channel. Channel merging happens in a goroutine using a select statement inside an infinite loop.
In This example, we create two int channels and merge them into a mediator channel. Then in a goroutine send an item to channels. In the end, we receive items in the mediator channel.
Constraints (Experimental)
Along with the previous features, Go came with a set of packages that implemented some of the basic functionalities. one of them is the constraints package. This package includes different numerical interface constraints.
For instance, constraints.ordered constraint to all the numbers.
in the above example, we create a function that accepts any numerical array and returns the sum.
Slices & Maps (Experimental)
Other packages are slices and maps, which include functionalities like sorting, containing, equality, and so on. for maps, there are functionalities like getting keys or values of a map and many other functions.
Limitations
There is some limitation to the new syntax, which may or may not be resolved in the future. I tried to list them all here.
Generic Method Declaration
currently, we can not define generic methods on structs.
Generic Type Instantiation for Method
method receiver must declare the type parameter otherwise, you will get a compile error.
Accessing Struct Type Parameter Fields
Another limitation is you cannot access struct fields or functions on the generic struct. if you want to do so, you have to declare them explicitly through interface constraints.
Declaration of Type Inside Generic Function
One limitation is that you cannot declare a new type inside a generic function. (I don’t know any use case where you need to define a new type inside a function! if you know, please let me know.)
Embedding Unnamed Type Parameter
One of the ways you can assign a field to a struct is unnamed fields. But for generic structs, you can not declare an unnamed Type Parameter.
Union Element With Method
If you want to use interface constraint in a union element, it must not have any functions. It makes sense because all the types in the union element must implement that specific functionality.
Complex Number
And for the last limitation, complex Number functions like real(), imag(), and complex() still don’t support generic input parameters.
Performance
Performance is one of the critical aspects of the new language change, which approximately stayed the same with little difference in nanoseconds order.
Conclusion
Currently, there is some new proposal for generics that includes language change. The official release note mentions that if it is necessary, syntax might change to fix a bug or an issue that users might raise.
I think using the basic generic features lightly is ok since it is impractical to change the whole generic syntax. In conclusion, Go version 2.X might be a better version to use generic on a large scale since it is more mature and errorless.