Generics in Go

By Syncfusion®, Inc. · Last updated

Go generics: type parameters, generic functions and return types, generic structs, the any and comparable constraints, custom constraints, and practical generic helpers.

What Are Generics?

Generics = writing a function or a type once, so it works with many types, while Go still checks every type.

Without generics, you might write:

  • SumInts(list []int) int
  • SumFloats(list []float64) float64
  • the same code twice, with only the type changed

With generics, you write one Sum[T Number](list []T) T.

Simple lesson: generics let you write one piece of code for many types, safely.

A Real-World Example

Think of a stencil for the school notice board.

  • One stencil has the shape of a star.
  • You can paint it in red, blue or gold.
  • The shape is the same every time. Only the colour changes.
  • But the stencil only takes paint, not glue or sand.

Now the mapping:

  • The stencil = the generic function.
  • The colour you choose = the type parameter, like int or string.
  • "Paint only" = the constraint: which types are allowed.

Generics were added in Go 1.18 (2022).

Older Go code often uses any and type assertions instead. Generics are safer, because Go checks the types before running. See Interfaces and Polymorphism.

What You Will Learn

In this lesson, you will learn:

  1. What generics and type parameters are.
  2. How generics give reusable code with type safety.
  3. How to write generic functions with generic parameters and return types.
  4. How to write generic structs.
  5. The constraints any, comparable and your own constraints.
  6. Practical examples: containers, utility functions and reusable algorithms.

Generic Fundamentals

Type Parameters

Type parameter = a placeholder for a type, written in square brackets: [T any].

func First[T any](list []T) T {
	return list[0]
}
  • [T any] = "T stands for some type. Any type is allowed."
  • Inside the function, T is used like a normal type.
  • When you call First([]string{"a"}), Go fills in T = string.

Reusable Code

Pick a type, and see what Go builds from the same code.

  • The code is written once.
  • Go checks it again for each type you use.

Type Safety

Type safety = Go checks that every value has the right type before the program runs.

any vs generics

Simple lesson: a type parameter like [T any] is a placeholder that Go fills in and checks for each call.

Generic Functions

Generic Parameters

One function, three types

package main

import "fmt"

func Last[T any](list []T) T {
	return list[len(list)-1]
}

func main() {
	fmt.Println(Last([]int{91, 78, 85}))
	fmt.Println(Last([]string{"Mon", "Tue", "Wed"}))
	fmt.Println(Last([]bool{true, false}))
}
85
Wed
false
  • Go works out T from the argument. This is type inference. See Data Types.
  • You can also write it yourself: Last[int](marks).

Generic Return Types

The return type can use the type parameter too.

Return a new slice of the same type

package main

import "fmt"

func Reverse[T any](list []T) []T {
	out := make([]T, len(list))
	for i, v := range list {
		out[len(list)-1-i] = v
	}
	return out
}

func main() {
	fmt.Println(Reverse([]int{1, 2, 3}))
	fmt.Println(Reverse([]string{"A", "B", "C"}))
}
[3 2 1]
[C B A]

More Than One Type Parameter

Map: change every element

package main

import (
	"fmt"
	"strings"
)

func Map[T, U any](list []T, f func(T) U) []U {
	out := make([]U, 0, len(list))
	for _, v := range list {
		out = append(out, f(v))
	}
	return out
}

func main() {
	names := []string{"asha", "ravi"}
	upper := Map[string, string](names, strings.ToUpper)
	lengths := Map[string, int](names, func(s string) int { return len(s) })
	fmt.Println(upper, lengths)
}
[ASHA RAVI] [4 4]
  • [T, U any] has two placeholders: the input type and the output type.
  • Here the type arguments are written out: Map[string, int]. This is always allowed, and it makes the code clear.

Simple lesson: put type parameters in [ ] after the function name, and use them for parameters and results.

Generic Types

Generic Structs

Generic type = a struct (or other type) with type parameters.

A stack that holds any one type

package main

import "fmt"

type Stack[T any] struct {
	items []T
}

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

func (s *Stack[T]) Pop() (T, bool) {
	var zero T
	if len(s.items) == 0 {
		return zero, false
	}
	top := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return top, true
}

func main() {
	books := &Stack[string]{}
	books.Push("Maths")
	books.Push("Science")
	top, ok := books.Pop()
	fmt.Println(top, ok)

	plates := &Stack[int]{}
	_, ok = plates.Pop()
	fmt.Println(ok)
}
Science true
false
  • Stack[string] and Stack[int] are two different types made from one definition.
  • Methods write the receiver as (s *Stack[T]).
  • var zero T gives the zero value of whatever T is.

Press Play to watch the stack.

Push and Pop on Stack[string]

Simple lesson: a generic struct like Stack[T] gives you one container design for every type.

Constraints

Constraint = the rule for which types a type parameter accepts.

any

  • [T any] = every type is allowed.
  • But you can only do things that work for every type: store it, return it, pass it on.
  • You cannot use + or < on it.

comparable

comparable = types that work with == and !=.

Count matches with comparable

package main

import "fmt"

func Count[T comparable](list []T, target T) int {
	n := 0
	for _, v := range list {
		if v == target {
			n++
		}
	}
	return n
}

func main() {
	votes := []string{"Asha", "Ravi", "Asha", "Asha"}
	dice := []int{6, 2, 6, 1}
	fmt.Println(Count(votes, "Asha"), Count(dice, 6))
}
3 2
  • == needs comparable. With any, Go would refuse the ==.
  • Map keys must be comparable too. See Maps.

Custom Constraints

A constraint is an interface. It can list types with |:

Sum numbers of any kind

package main

import "fmt"

type Number interface {
	~int | ~int64 | ~float64
}

func Sum[T Number](list []T) T {
	var total T
	for _, v := range list {
		total += v
	}
	return total
}

type Rupees int

func main() {
	fmt.Println(Sum([]int{10, 20, 30}))
	fmt.Println(Sum([]float64{1.5, 2.25}))
	fmt.Println(Sum([]Rupees{100, 250}))
}
60
3.75
350
  • int | int64 | float64 = "T must be one of these".
  • ~int = "int, or any type built on int", like Rupees. See Advanced Type Design.
  • Because every allowed type supports +, Go lets you write total += v.

A constraint can also ask for methods:

A constraint with a method

package main

import "fmt"

type Labeler interface {
	Label() string
}

type Book struct{ Title string }

func (b Book) Label() string { return "Book: " + b.Title }

func PrintAll[T Labeler](items []T) {
	for _, it := range items {
		fmt.Println(it.Label())
	}
}

func main() {
	PrintAll([]Book{{"Gitanjali"}, {"Malgudi Days"}})
}
Book: Gitanjali
Book: Malgudi Days

Go 1.21 added the cmp.Ordered constraint (all types that work with <), and the slices and maps packages full of generic helpers.

This lab does not run those packages, so the examples write their own constraints. On your computer, prefer the standard ones.

Simple lesson: any allows everything, comparable allows ==, and your own constraints list types or methods.

Practical Examples

Generic Containers

A Pair that holds two different types

package main

import "fmt"

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

func main() {
	p1 := Pair[string, int]{"Asha", 91}
	p2 := Pair[int, string]{8, "Class 8"}
	fmt.Println(p1.Key, p1.Value)
	fmt.Println(p2.Key, p2.Value)
}
Asha 91
8 Class 8

Generic Utility Functions

Filter: keep what matches

package main

import "fmt"

func Filter[T any](list []T, keep func(T) bool) []T {
	var out []T
	for _, v := range list {
		if keep(v) {
			out = append(out, v)
		}
	}
	return out
}

func main() {
	marks := []int{91, 34, 78, 25, 66}
	passed := Filter(marks, func(m int) bool { return m >= 35 })
	fmt.Println(passed)

	names := []string{"Asha", "Ravi", "Aman"}
	aNames := Filter(names, func(s string) bool { return s[0] == 'A' })
	fmt.Println(aNames)
}
[91 78 66]
[Asha Aman]

Type-Safe Collections

  • Stack[string] will only ever hold strings.
  • stack.Push(5) on a Stack[string] is a build error, not a runtime surprise.
  • That is the main gain over a []any list.

Reusable Algorithms

Find the largest, for any ordered type

package main

import "fmt"

type Ordered interface {
	~int | ~float64 | ~string
}

func Largest[T Ordered](list []T) T {
	best := list[0]
	for _, v := range list[1:] {
		if v > best {
			best = v
		}
	}
	return best
}

func main() {
	fmt.Println(Largest([]int{72, 95, 88}))
	fmt.Println(Largest([]float64{1.5, 9.25, 3.0}))
	fmt.Println(Largest([]string{"kiwi", "mango", "apple"}))
}
95
9.25
mango
  • > works because every type in Ordered supports it.
  • Strings compare in dictionary order, so "mango" is largest.

Easy way to remember

  • Same code, different types → generics.
  • Different behaviour per type → interfaces.
  • Start with any. Tighten the constraint only when you need ==, < or +.

Simple lesson: use generics for containers and helpers that do the same thing for many types.

TL;DR

  1. Generics = write once, use with many types, still type-checked. Added in Go 1.18.
  2. Type parameter = [T any] after the name. Go fills T in.

    • Type inference = Go works out T from the arguments.
    • Or write it: Map[string, int](...).
  3. Generic function = func Last[T any](list []T) T.
  4. Generic struct = type Stack[T any] struct { items []T }.

    • Methods: func (s *Stack[T]) Push(v T).
  5. Constraints:

    • any = all types.
    • comparable = types with ==.
    • Custom = interface { ~int | ~float64 } or method lists.
    • ~int = int and types built on int.
  6. Uses = containers, Map, Filter, Sum, Largest.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
a > b with [T any]Any type can be compared> needs an ordered constraintcannot compare a > b (T is not ordered)
v == x with [T any]any supports ==Use comparableincomparable types in type set
`intfloat64without~and passingRupees`Built-on types are includedAdd ~ to include themRupees does not satisfy Number
Generics for everythingGeneric code is always betterUse them when the code is the same for many typesHard-to-read code.
Using any lists instead of genericsThey are the sameany needs assertions and can panicRuntime errors.
var zero T = nilEvery type can be nilUse var zero TA build error for value types.

Check Yourself

Exercises

What to Remember

  • Generics let one function or type work with many types, still checked by Go.
  • Type parameters go in [ ]: [T any]. Go usually infers them.
  • any allows all types, comparable allows ==, custom constraints list types or methods.
  • ~int includes types built on int.
  • Use generics for containers and helpers. Use interfaces when behaviour differs per type.

Next: Collections of Structs. You will learn how to keep, search, sort and update many records, like a whole class of students.