Golang Generics

Introduction video: https://youtu.be/Pa_e9EeCdy8?si=IvEhP5dFZWIyYljs

The summary is as below:

Type Constraint

Specifying type constraint after the type parameter T.

1
func min[T constraints.Ordered] (x,y T) T

The type constraints are interfaces, for example the existing ordered constraints: https://pkg.go.dev/golang.org/x/exp/constraints#Ordered

Define your own type constraint:

1
2
3
4
5
// Integer is another type constraint interface
type X interface {
Integer|~string
}
// No method is with the type constraint interface.

The ~string means any type that uses string as underlying type, for example:

1
type Line string

Type Constraint Literal

1
2
3
4
5
6
7
8
9
10
11
// it is common to write type constraint literal "in line".

// interface{~[]E}: requires slice of underlying type E and E is not further
// constrained with interface{}:
[S interface{~[]E}, E interface{}]

// it is common to remove interface{} around ~[]E, can be rewritten as:
[S ~[]E, E interface{}]

// further, using interface{} alias any:
[S ~[]E, E any]

Struct with Generic

This is an example about how to define generic struct:

1
2
3
4
5
6
7
8
9
10
// a generic binary tree, can be Treep[T any ]
type Tree[T interface{}] struct {
left, right *Tree[T]
data T
}

func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }

// Use Tree with type string
var stringTree Tree[string]

Type Inference

The type argument float64 can be inferred from the arguments a and b:

1
2
3
4
5
func min[T constraints.Ordered](x, y T) T

var a, b, m float64

m = min(a,b)

Example about Complex Type Inference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type Point []int32

func ScaleAndPrint(p Point) {
// calls scale[Point, int32], the int32 is inferred from p, because 2 does not
// tell us anything, it is just under Integer constraint but we don't know its
// exact type.
r := Scale(p, 2)
fmt.PrintLn(r.String())
}

// S has type constraint in terms of type parameter E, if we know the type of S
// we know the type of E.
func Scale[S ~[]E, E constraints.Integer](s S, sc E) S {
r := make(S, len(s))
for i, v := range s {
r[i] = v * c
}
return r
}

// This is the problematic generic func, the fmt.PrintLn(r.String()) will not
// compile because it returns []int32 and []int32 does not have String() method.
func Scale[E constraints.Integer](s []E, c E) []E {
r := make([]E, len(s))
for i, v := range s {
r[i] = v * c
}
return r
}

When to Use Generics

Please also use your own judgement:

  • Functions that work on slices, maps and channels of any element type.
  • General purpose data structures.
    • When operating on type parameters, prefer functions over methods.
  • When a method looks the same for all types.
0%