Generics allow us to write functions that can be used with arguments of any data type. The latest version of Go has introduced generics into the language. Therefore, in this article I'd like to explore the usage of generics in Go.
Before Go v1.18
The were no generics in the older versions of Go. However one workaround for generics in older versions of Go is the empty interface, interface{}
in combination with type assertion and reflection. For example, lets say we want to write a function that would take an array of any type and sum up the array, in older versions of Go we would have to do something like this:
func sumAnyType(i interface{}) (o interface{}) {
value := reflect.ValueOf(i)
if reflect.TypeOf(i).Kind() != reflect.Slice || value.Len() < 1 {
return
}
switch value.Index(0).Kind() {
case reflect.Int:
var res int = 0
for i := 0; i < value.Len(); i++ {
res += value.Index(i).Interface().(int)
}
return res
case reflect.Float64:
var res float64 = 0
for i := 0; i < value.Len(); i++ {
res += value.Index(i).Interface().(float64)
}
return res
}
return
}
func main() {
log.Println(sumAnyType([]int{1, 2, 3})) // prints 6
log.Println(sumAnyType([]float64{1.2, 2.5, 3.9})) // prints 7.6
log.Println(sumAnyType([]float32{1.2, 2.5, 3.9})) // prints nil
}
Although the above works, there are some downsides one of which is code repetition. We are writing the summing logic for each data types and if the logic for particular data type is not written, the array of that type cant be summed.
Go v1.18 Generics
With the introduction of generics in the latest version of Go, the above example can be simplified to the following:
func sumAnyType[T int | float64](i []T) (o T) {
for _, v := range i {
o += v
}
return
}
func main() {
log.Println(sumAnyType([]int{1, 2, 3})) // prints 6
log.Println(sumAnyType([]float64{1.2, 2.5, 3.9})) // prints 7.6
// log.Println(sumAnyType([]float32{1.2, 2.5, 3.9})) // does not compile
}
As you see above, Go generics greatly simplifies the codes and enforces the Don't Repeat Yourself (DRY) principle as we are able to use the same function for multiple data types without having to rewrite the same logic for each data types.
Now that we understand the significance of generics in Go, lets understand the various features introduced for generics in Go v1.18:
1. The any
keyword
The any
keyword is just an alias to the empty interface, interface{}
. That means, anything that we can do with interface{}
can also be done with any
. For example:
func printType(i any) {
_, ok := i.(string)
if ok {
log.Println("its a string")
}
_, ok = i.(int)
if ok {
log.Println("its a integer")
}
}
2. Type parameters, type arguments and type constraints
Type parameters is what allows a function to be generic, allowing it to work with arguments of different types. We'll call the function with type arguments and the ordinary function arguments. Type constraint defines a list of types a function can receive.
3. The comparable
type costraint
comparable
is a predeclared type constraint in Go v1.18 that denotes the types that can be compared using ==
or !=
.
4. Type constraint as an interface
The type constraint can also be declared as an interface so that it can be reused. For example:
type CustomConstraint interface {
int | float64
}
func addOne[T CustomConstraint](i T) (o T) {
o = i + 1
return
}
func multiply2[T CustomConstraint](i T) (o T) {
o = i * 2
return
}
func main() {
log.Println(addOne[int](1))
log.Println(multiply2[float64](3.14))
}
5. The ~
keyword
A type constraint can be prefixed with ~
to restrict all the custom types whose underlying type is the same as the constraint.
The following does not compile because CustomConstraint
only restrict type int
:
type CustomInt int
type CustomConstraint interface {
int
}
func addOne[T CustomConstraint](i T) (o T) {
o = i + 1
return
}
func main() {
log.Println(addOne[CustomInt](1))
}
The following compiles successfully because CustomConstraint
restricts int
and all the other custom types whose underlying type is int
:
type CustomInt int
type CustomConstraint interface {
~int
}
func addOne[T CustomConstraint](i T) (o T) {
o = i + 1
return
}
func main() {
log.Println(addOne[CustomInt](1))
}
As a conclusion, Go generics is really useful especially to enforce DRY and can be used for various use cases. In my opinion, generics in Go is simpler and easier to understand compared to other languages such as Java.
Additional resources:
Top comments (0)