Introduction to the new generics in Go (type parameters)

It seems that Go will be adding type parameters and generic types & functions in 1.18, expected to be released Feb 1, 2022. Here is a short tutorial on how to use them, based on Google’s extensive spec. I think it’s a lot better than what they were going to do with contracts.

Example

Representing graphs:

// A Node is a thing with edges
type NodeOn[Edge any] interface {
	Edges() []Edge
}

// An Edge is a thing with two nodes
type EdgeOn[Node any] interface {
	Nodes() (from, to Node)
}

// A graph has compatible nodes and edges!!
type Graph[Node NodeOn[Edge], Edge EdgeOn[Node]] struct { ... }

// Makes a new graph from a list of nodes. Must take type parameters itself.
func New[Node NodeOn[Edge], Edge EdgeOn[Node]] (nodes []Node) *Graph[Node, Edge] {
	...
}

// A method can have a parameterized receiver!
func (g *Graph[Node, Edge]) ShortestPath(from, to Node) []Edge { ... }

// Instantiating and using it:

type Vertex struct { ... }
func (v *Vertex) Edges() []*FromTo { ... }
type FromTo struct { ... }
func (ft *FromTo) Nodes() (*Vertex, *Vertex) { ... }
var g = graph.New[*Vertex, *FromTo]([]*Vertex{ ... })

// Note that *Vertex and *FromTo are not interface types,
// but they match the constraints!

Generic map function for slices:

// Definition:
func Map[F, T any](s []F, f func(F) T) []T {
    result := make([]T, len(s))
    for i, x := range s {
        result[i] = f(x)
    }
    return result
}

// Using it:
s := []int{1,2,3}
f := func(i int) int64 { return int64(i * 3) }
// Explicitly give the type parameters:
r1 := Map[int, int64](s, f)
// Sometimes the compiler can infer the type parameters: 
r2 := Map(s, f)

Overview

The compiler only permits you to use methods/operations defined on all types in the constraint, so this will fail:

// This will fail
func Add[T any](x, y T) T {
    return x + y // Error: some types in `any` do not have `+` defined
}

How to define constraints in general

All contraints are interface types. If you put a list of types or you embed a constraint in an interface, then your interface is now a constraint, and you have to make it a type parameter to any functions that use it.

// Embedding one constraint and adding a method
type ComparableHasher interface {
	comparable
	Hash() uintptr
}

// You can have one type list in a constraint
type Inty interface {
	type int, int8, int16, int32, int64
}

When not to use type parameters

It’s better to use regular interfaces if you can! Like here you don’t need any generic stuff:

type Stringer interface {
	String() string
}

// Still do this!
func foo(t Stringer) string {
	return t.String()
}

// Bad:
func foo[T Stringer](t T) string {
	return t.String()
}

You might get faster run (and slower build) with the unnecessary type parameters, because you’ll get a dedicated function for each type, but it is confusing.

Interfaces may be nil but value types cannot be, the value of a type-parameter-type parameter will never be nil.

Why use when there are interfaces?

Well before you couldn’t write e.g. an interface that captures numbers and not strings. You had to write the same function over and over. There were no sum types in Go but now there basically will be.

More importantly IMO, data structures defined by their structure instead of contents are possible. Like the graph example above.

Can I branch/case/switch on the type of the generic inside a function?

No, not really. This is the closest you can do:

func NewtonSqrt[T Float](v T) T {
	var iterations int
	switch (interface{})(v).(type) {
	case float32:
		iterations = 4
	case float64:
		iterations = 5
	default:
		panic(fmt.Sprintf("unexpected type %T", v))
	}
	...
}

But you can use it as the type:

func Switcher[T Inty](v interface{}) int {
	switch v.(type) {
	case T:
		return 0
	case string:
		return 1
	default:
		return 2
	}
}

Can I have generic/parameterized methods?

Not really! The receiver can have type parameters, but the method cannot add any.

For more information, see the spec.