Generics in Go

Tsvetan Dimitrov

Agenda

  • What are Generics?

  • Type Parameters

  • Type Constraints

  • Type Approximation

  • When to use Generics?

What are Generics?

Definition

  • Write data structures and functions by specifying types later.

  • Generic functions have type parameters.

  • Specific Go types and user defined struct types can also have type parameters.

Non generic function

func reverse(s []int) []int {
        l := len(s)
        rev := make([]int, l)

        for i, item := range s {
        	rev[l-i-1] = item
        }
        return r
}

Generic Function

func reverse[T any](s []T) []T {
        l := len(s)
        rev := make([]T, l)

        for i, item := range s {
        	rev[l-i-1] = item
        }
        return r
}

Generics Syntax

func reverse[T any](s []T) []T

Type Parameter

Type Constraint

Calling a generic function

reverse([]int{1, 2, 3, 4, 5})
reverse[int]([]int{1, 2, 3, 4, 5})

Without type argument

With type argument

Type Parameters

Functions

func print[T any](v T){
	fmt.Println(v)
}

interface-based definition derived from usage patterns and some other conditions

separate definition for each combination of types passed at instantiation

Compiler

Slices

func ForEach[T any](s []T, f func(item T, i int , s []T)) {
    for i, item := range s {
        f(item,i,s)
    }
}
  • When creating a slice, only one type is required, so only one type parameter is necessary.

Maps

func Keys[K comparable, V any](m map[K]V) []K {
    // creating a slice of type K with length of map
    key := make([]K, len(m))
    i := 0
    for k, _ := range m {
        key[i] = k
        i++
    }
    return key
}
  • Map key types are mandatory to be comparable.

  • comparable is a built-in constraint that constrains type arguments to types that are comparable, i.e. work with == and != operators.

Structs

// T is type parameter here, with any constraint
type MyStruct[T any] struct {
    item T
}

// No new type parameter is allowed in struct methods
func (m *MyStruct[T]) Get() T {
    return m.item
}
func (m *MyStruct[T]) Set(v T) {
    m.item = v
}
  • Type parameters are NOT allowed in struct methods.

  • Defined type parameters in structs are usable in methods.

Generic Types / 1

  • Generic types can be nested within other types.

  • The type parameter defined in a function or struct can be passed to any other type with type parameters.

Generic Types / 2

type Pair[K, V any] struct {
	Key   K
	Value V
}

func pairs[K comparable, V any](m map[K]V) []*Pair[K, V] {
	result := make([]*Pair[K, V], len(m))
	i := 0
	for k, v := range m {
		newPair := &Pair[K, V]{
			Key:   k,
			Value: v,
		}
		result[i] = newPair
		i++
	}
	return result
}

Type Constraints

Definition

  • Go generics are only allowed to perform specific operations listed in an interface, known as a constraint.

  • The compiler uses it to check if the provided type supports all the operations performed by values instantiated using the type parameter.

Constraints with methods

type Stringer interface {
	String() string
}

func stringer[T Stringer](s T) string {
	return s.String()
}
  • Stringer is a contstraint.

  • T can only perform operations defined on Stringer.

Constraints with predefined types / 1

type Number interface {
	int
}
  • Predefined types like int and string implement interfaces that are used in constraints.

Constraints with predefined types / 2

type Number interface {
        int | int8 | int16 | int32 | int64 | float32 | float64
}
  • Union operator | allows a union of types (i.e., multiple concrete types can implement the single interface and the resulting interface allows for common operations on all union types).

Constraints with predefined types / 3

func Max[T Number](x, y T) T {
        if x > y {
                return x
        }
        return y
}

 

  • T as a type parameter now supports every int, float type.

  • The constraint should be only implementing types that support arithmetic operations (<, >, +, -, etc.).

Constraints Package

func Max[T constraints.Ordered](x, y T) T {
        if x > y {
                return x
        }
        return y
}

 

  • constraints is a standard package that contains constraints for Integer, Float etc.

  • constraints.Ordered defines all the types that support >,<,==, and != operators.

Type Approximation

Definition / 1

type Number interface {
        ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64
}
  • ~ operator allows us to specify that the interface also supports types with the same underlying predefined types.

Definition / 2

// Type with underlying int
type Point int

func Max[T Number](x, y T) T {
        if x > y {
                return x
        }
        return y
}

Inline Constraints

func Max[T ~int | ~float32 | ~float64](x, y T) T {
        if x > y {
                return x
        }
        return y
}
  • Union operator (|) and type approximation operator (~) both used together without an interface.

     

Nested Constraints

type Integer interface {
        ~int | ~int8 | ~int16 | ~int32 | ~int64
}

type Float interface {
        ~float32 | ~float64
}

type Number interface {
        Integer | Float
}
  • Number is built from Integer and Float constraints.

     

When to use generics?

Write code, don’t design types?!?!!

Good Use Cases

  1. Functions that work on slices, maps and channels of any element type.

  2. General purpose data structures (not built into the language, e.g. linked list or binary tree).

  3. When a method is the same for all types.

Bad Use Cases

  1. When just calling a method on the type argument.

  2. When a method implementation is different for each type.

  3. When the operation is different, even without a method, e.g. encoding/json package.

Summary

  • Defined Generics Usage.

  • Introduction of Go type parameters and type constraints.

  • Advice on how and when to use generics in favor of interfaces or reflection.

Questions?

Generics in Go

By Tsvetan Dimitrov

Generics in Go

Introduction to generics in Go since version 1.18.

  • 150