Go Generics

Go Generics/Type Parameters by programmingpercy

Summary: Overall, Go generics (also called as type parameters) is good, but not so good. Generics on types & functions are supported, but methods are not - which causes a lot of limitations.

This post is just my very first feelings about the generics feature of Go after some simple experimentation, it's not a full picture of what generics in Go looks like, if you want to see the full picture, check this very long spec.

The Good Things #

Now you don't need to write function like max again and again for different data types:

func max[T constraints.Ordered](a T, b T) T {
if a > b {
return a
}
return b
}

max(1, 2) // 2
max(4.5, 3.9) // 4.5

Playground

And you can write a generics tree which can be only achieved in the past using interface{}

type tree[T any] struct {
Left *tree[T]
Right *tree[T]
Value T
}

t := tree[int]{}
t1 := tree[string]{}

Playground

Typed parameters can be named or unnamed

// Index returns the index of e in s, or -1 if not found.
func Index[T interface{ Equal(T) bool }](s []T, e T) int {
for i, v := range s {
if e.Equal(v) {
return i
}
}
return -1
}

// Index returns the index of e in s, or -1 if not found.
func Index1[T Equaler[T]](s []T, e T) int {
for i, v := range s {
if e.Equal(v) {
return i
}
}
return -1
}

type Equaler[T any] interface {
Equal(T) bool
}

Playground

We can also union multiple types together

type numbers interface {
int | int8 | int32 | int64 | float32 | float64
}

func min[T numbers](a, b T) T {
if a > b {
return b
}
return a
}

min(2,3) // 2
min(2.4, 0.5) // 0.5

Playground

~ operator can be used to "include" all underlying types

type degree int

type numbers interface {
~int | ~int8 | ~int32 | ~int64 | ~float32 | ~float64
}

func min[T numbers](a, b T) T {
if a > b {
return b
}
return a
}

min(degree(2), degree(3)) // 2
min(2.4, 0.5) // 0.5

Playground

comparable interface can be used for writing comparable functions:

func equal[T comparable](a, b T) bool {
return a == b
}

equal(degree(2), degree(3)) // false
equal(degree(2), degree(2)) // true
equal(2.4, 0.5) // false
equal(2.3, 2.3) // true

Playground

The Limitations #

Methods cannot be generics

What does it mean when methods cannot be generics? It means you will not be able to write a map or reduce API like Java. The main reason for not supporting this feature is because of typed parameters only known by the compiler, the runtime has no ideas about type parameters and this introduces some complexity for the instantiation during run time .

You can see the full explanation from the Go team here, it's pretty much well explained. But I personally don't really agree with them when they say "one of the main roles of methods is to permit types to implement interfaces" and used this to argue about not including generics on methods. To me, methods are more important than just to "implement" an interface.

So any alternative way for writing map/reduce like API? Yes, use functions instead.

// Map turns a []T1 to a []T2 using a mapping function.
// This function has two type parameters, T1 and T2.
// This works with slices of any type.
func Map[T1, T2 any](s []T1, f func(T1) T2) []T2 {
r := make([]T2, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}

// Reduce reduces a []T1 to a single value using a reduction function.
func Reduce[T1, T2 any](s []T1, initializer T2, f func(T2, T1) T2) T2 {
r := initializer
for _, v := range s {
r = f(r, v)
}
return r
}

More Detail

Mixed types cannot be inferred as any

Typed parameters mean all parameters must be the same type. And the inference rule of Go is that it will check the type of the first parameter, others must follow the same. What does it mean? It means if you don't declare the first parameter as anyexplicitly, it will never be inferred as any.

func Print[T any](t ...T) {}

//
var a any = 1
Print(a, "2") // VALID as all parameters will be inferred as "any".
Print[any](1, "2") // VALID as we provided the type parameter as "any".
Print(1, "2") // INVALID since its expect all parameters as int and "2" is not an int.

Playground

Although we can provide the type parameter when calling the method, but it is kind of inconvenient.

[T ~[]any] and [S ~[]T, T any] Are Different

This is not really a limitation, it is just a mistake people who first started with generics often face. The difference between the two declarations is the first one declares anyas a specific type, the latter declares it as a parameter type.

[T ~[]any] only accepts []any, but [S ~[]T, T any] will accept any kind of slices, such as: []int, []string, []any,...

Interfaces with methods can be used as a typed parameters, but cannot be "union"

Yes, it is. But what's the point of having union interfaces? I don't really know the usage of this case.