mdawar.dev

A blog about programming, Web development, Open Source, Linux and DevOps.

Go - Generics

With generics, functions and types can be used with multiple types using type parameters.

Type Parameters

Type parameters are placeholders for the types that are provided when using the generic code.

A type parameter list declares the type parameters enclosed in square brackets [].

go
// A type parameter is declared with a name and a constraint.
// The name declares a type parameter that acts as a placeholder for an unknown type.
// By convention type parameter names are capital single letters.
// In this example, `T` is the type parameter and `any` is the type constraint.
[T any]

// A type constraint is an interface.
[T interface{}] // Any type is allowed when using the empty interface
[T any]         // `any` is an alias for the empty interface
[V comparable]  // comparable: built-in constraint for types that support comparison

// Constraint defined as a union of types.
// Any type in the union will be permitted in the calling code.
[V int64 | float64] // = [V interface{ int64 | float64 }]

// Constraint representing all types with a specific underlying type.
// `~T` is the set of all types whose underlying type is `T`.
[P ~int] // = [P interface{ ~int }]

// Multiple type parameters.
[K comparable, V int64 | float64]

// Type constraint refering to another type parameter.
[S []E, E any]

// The blank identifier can be used as the type parameter name
// if the type is not used but still required.
[_ any]

Generic Functions

go
// Type parameters of a function are declared between brackets []
// before the function's arguments.
func GenericFunction[T any](param T) {
	// Implementation
}

// This function will work with any type that supports comparison.
// `comparable` is a built-in constraint useful for comparing values
// of a type using the == and != operators.
func Index[T comparable](s []T, x T) int {
	// Can use == and != operators only for comparable types
}

// A constraint can be defined as a union of types.
// Any type in the union will be permitted in the calling code.
func Sum[K comparable, V int64 | float64](m map[K]V) V {
	// Implementation
}

// The type parameter is replaced with a type argument on instantiation
// of the generic function or type.
GenericFunction[string]("Hello World")

Index[string]([]string{"a", "b", "c"}, "b")

// The type argument can be omitted when the compiler can infer the type
// from the function's arguments.
GenericFunction("Hello World")      // GenericFunction[string]
GenericFunction(10)                 // GenericFunction[int]
Index([]string{"a", "b", "c"}, "b") // Index[string]
Index([]int{1, 2, 3}, 5)            // Index[int]

Type Constraints

A type constraint is an interface that defines the set of permissible type arguments for the type parameter.

Other than being a method specification, an interface element may also be:

  1. An arbitrary type term T
  2. A term of the form ~T specifying the underlying type T
  3. A union of terms T1 | T2 | ~T3
go
// Interface representing only the type `int`.
type Integer interface {
	int
}

// Interface representing all types with underlying type `int`.
type AnyInt interface {
	~int
}

// Example of a named type with an underlying type `int`.
// It satisfies the `~int` constraint but does not satisfy the `int` constraint.
type MyInt int

// Interface representing all types with underlying type `int`
// that implement the `String` method.
type SI interface {
	~int
	String()
}

// Interface representing a union of the types `[]byte` and `string`.
type S interface {
	[]byte | string
}

// Interface representing a union of the types `int64` and `float64`.
type N interface {
	int64 | float64
}

// Interface representing all floating point types including
// any named types with underlying types either `float32` or `float64`.
type Float interface {
	~float32 | ~float64
}

These interfaces may only be used as type constraints.

go
// Float interface to be used as a type constraint.
type Float interface {
	~float32 | ~float64 // All floating point types
}

// Generic function that can be used with types defined in the `Float` constraint.
func Sum[T Float](s []T) T {
	// Implementation
}

// Cannot be used as types.
var x Float // illegal: cannot use type Float outside a type constraint

A constraint can be an interface literal.

go
// Constraint specifying a union of types.
func Sum[T interface{ int64 | float64 }](s []T) T {
	// Implementation
}

// Constraint specifying an underlying type.
func Max[T interface{ ~int }]() T {
	// Implementation
}

In a type parameter list, if the constraint is an interface literal the enclosing interface{} may be omitted for convenience.

go
// The enclosing interface{} is omitted for convenience.
func Sum[T int64 | float64](s []T) T {
	// Implementation
}

func Max[T ~int](n ...T) T {
	// Implementation
}

// It cannot be omitted if the interface specifies methods.
func Display[T interface {
	~int
	String()
}](x T) {
	// Implementation
}

Generic Types

A generic type specifies type parameters in the type definition.

go
// Generic type with a type parameter list.
type Stack[T any] struct {
	values []T
}

// The method receivers must declare the same number of type parameters.
func (s *Stack[T]) Push(value T) {
	// Implementation
}

func (s *Stack[T]) Pop() (T, bool) {
	// Implementation
}

The type arguments are provided in square brackets []:

go
// All type arguments must always be provided explicitly.
var i Stack[int]    // Stack of ints
var s Stack[string] // Stack of strings

Generic Type Zero Value

Functions returning the zero value of a generic type can declare a variable with that type and return it.

go
func ZeroValue[T any]() T {
	var zero T
	return zero
}

ZeroValue[int]()    // 0
ZeroValue[bool]()   // false
ZeroValue[string]() // ""