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) intSumFloats(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
intorstring. - "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:
- What generics and type parameters are.
- How generics give reusable code with type safety.
- How to write generic functions with generic parameters and return types.
- How to write generic structs.
- The constraints
any,comparableand your own constraints. - 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,
Tis used like a normal type. - When you call
First([]string{"a"}), Go fills inT = 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
Tfrom 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]andStack[int]are two different types made from one definition.- Methods write the receiver as
(s *Stack[T]). var zero Tgives the zero value of whateverTis.
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
==needscomparable. Withany, 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", likeRupees. See Advanced Type Design.- Because every allowed type supports
+, Go lets you writetotal += 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 aStack[string]is a build error, not a runtime surprise.- That is the main gain over a
[]anylist.
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 inOrderedsupports 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
- Generics = write once, use with many types, still type-checked. Added in Go 1.18.
Type parameter =
[T any]after the name. Go fillsTin.- Type inference = Go works out
Tfrom the arguments. - Or write it:
Map[string, int](...).
- Type inference = Go works out
- Generic function =
func Last[T any](list []T) T. Generic struct =
type Stack[T any] struct { items []T }.- Methods:
func (s *Stack[T]) Push(v T).
- Methods:
Constraints:
any= all types.comparable= types with==.- Custom =
interface { ~int | ~float64 }or method lists. ~int= int and types built on int.
- Uses = containers,
Map,Filter,Sum,Largest.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong | |
|---|---|---|---|---|
a > b with [T any] | Any type can be compared | > needs an ordered constraint | cannot compare a > b (T is not ordered) | |
v == x with [T any] | any supports == | Use comparable | incomparable types in type set | |
| `int | float64without~and passingRupees` | Built-on types are included | Add ~ to include them | Rupees does not satisfy Number |
| Generics for everything | Generic code is always better | Use them when the code is the same for many types | Hard-to-read code. | |
Using any lists instead of generics | They are the same | any needs assertions and can panic | Runtime errors. | |
var zero T = nil | Every type can be nil | Use var zero T | A 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. anyallows all types,comparableallows==, custom constraints list types or methods.~intincludes types built onint.- 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.