Generics in Go: Type Parameters, Constraints and a Type-Safe Stack
Chapter 05 of 06

Generics in Go: Type Parameters, Constraints and a Type-Safe Stack

Type parameters, any, comparable, union and ~ constraints, cmp.Ordered, a generic Stack type, and when to prefer interfaces instead.

Sabbir Ahmmed3 min read0
On this page

Generics (added in Go 1.18) let you write one function or type that works with many types, while keeping full compile-time type safety. Before generics you had to duplicate code for every type or fall back to interface{} and lose type checking.

Type parameters

A generic function declares type parameters in square brackets. Each one has a constraint that says which types are allowed.

go
1
2
3
4
5
6
7
8
9
func PrintAll[T any](items []T) {
	for i, item := range items {
		fmt.Println(i, "->", item)
	}
}

PrintAll([]string{"apple", "banana"})
PrintAll([]int{1, 2, 3})
PrintAll([]bool{true, false})

The compiler infers T from the argument, so you rarely write PrintAll[int](...) explicitly.

Constraints: any, comparable and unions

any (alias of interface{}): any type at all. You can store and pass the values, but not use operators like + or == on them.
comparable: types that support == and != (numbers, strings, booleans, pointers, channels, arrays and structs of comparable fields). Needed for map keys and equality checks.
Union constraints list allowed types with |.
go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Only int or string slices are allowed
func PrintStrInt[T int | string](items []T) {
	for _, item := range items {
		fmt.Println(item)
	}
}

PrintStrInt([]int{1, 2})
// PrintStrInt([]bool{true}) // compile error: bool does not satisfy int | string

// comparable lets us use ==
func Index[T comparable](items []T, target T) int {
	for i, v := range items {
		if v == target {
			return i
		}
	}
	return -1
}

fmt.Println(Index([]string{"a", "b"}, "b")) // 1

Reusable constraints and the ~ operator

Name a constraint as an interface to reuse it. The tilde ~ means "this type or any type whose underlying type is this type":

go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Number interface {
	~int | ~int64 | ~float64
}

func Sum[T Number](values []T) T {
	var total T // zero value of T
	for _, v := range values {
		total += v // + is allowed: every type in Number supports it
	}
	return total
}

type Celsius float64

fmt.Println(Sum([]int{1, 2, 3}))         // 6
fmt.Println(Sum([]float64{1.5, 2.5}))    // 4
fmt.Println(Sum([]Celsius{10, 20.5}))    // 30.5, works because of ~float64

Without ~, Celsius would be rejected even though it is "just a float64". The standard library provides cmp.Ordered for every type that supports <:

go
1
2
3
4
5
6
7
8
9
10
import "cmp"

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

fmt.Println(Max(3, 7), Max("go", "rust")) // 7 rust

Generic types: a type-safe stack

Types can have type parameters too. The original repository declared a generic Stacks struct; here it is with methods, so it is actually useful:

stack.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Stack[T any] struct {
	elements []T
}

func (s *Stack[T]) Push(v T) {
	s.elements = append(s.elements, v)
}

func (s *Stack[T]) Pop() (T, error) {
	var zero T
	if len(s.elements) == 0 {
		return zero, errors.New("stack is empty")
	}
	last := s.elements[len(s.elements)-1]
	s.elements = s.elements[:len(s.elements)-1]
	return last, nil
}

func (s *Stack[T]) Len() int { return len(s.elements) }
go
1
2
3
4
5
6
7
8
9
10
var tasks Stack[string]
tasks.Push("write tests")
tasks.Push("deploy")

next, _ := tasks.Pop()
fmt.Println(next) // deploy

var nums Stack[int]
nums.Push(42)
// nums.Push("x") // compile error: cannot use "x" as int

Note var zero T: in generic code you cannot write 0 or "" because you do not know the type, so you declare a variable to get its zero value.

Generics in the standard library

You may not need to write generics often, because the standard library already uses them:

go
1
2
3
4
5
6
7
8
9
10
11
12
import (
	"maps"
	"slices"
)

nums := []int{3, 1, 2}
slices.Sort(nums)                 // [1 2 3]
fmt.Println(slices.Contains(nums, 2)) // true
fmt.Println(slices.Max(nums))         // 3

m := map[string]int{"b": 2, "a": 1}
keys := slices.Sorted(maps.Keys(m))   // [a b]

When (not) to use generics

Use generics for data structures (stacks, caches, sets) and algorithms over collections (map, filter, sum, sort) where the logic is identical for every type.

Prefer interfaces when behaviour differs per type (for example, each payment gateway charges differently). If you find yourself writing a type switch inside a generic function, an interface is probably the better tool.

Tip
Start with concrete types. Introduce generics when you actually have the second or third copy of the same code, not before.

Key takeaways

Type parameters go in [] and each has a constraint.
any allows everything; comparable allows ==; unions (int | string) list types.
~T includes custom types built on T; cmp.Ordered covers <.
Generic types like Stack[T] give compile-time type safety without duplication.
Use generics for identical logic across types, interfaces for different behaviour.
#Golang#Generics

Last updated on

Discussion0