Generics let you write one function or type that works with many types, while the compiler still checks every call. In this lesson you’ll write generic functions, pick the right constraint (any, comparable, cmp.Ordered or your own), build a generic Stack[T], and learn when a plain function or an interface is the better choice.
The problem generics solve
Say you need the sum of a slice of int values, and later the sum of a slice of float64 values. Without generics you write the same function twice:
package main
import "fmt"
func SumInts(nums []int) int {
var total int
for _, n := range nums {
total += n
}
return total
}
func SumFloats(nums []float64) float64 {
var total float64
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(SumInts([]int{1, 2, 3}))
fmt.Println(SumFloats([]float64{1.5, 2.5}))
}Code language: Go (go)
6
4Code language: plaintext (plaintext)
The two bodies are identical. Only the types differ. You could take an any parameter instead (see Interfaces), but then you lose type checking and need type assertions everywhere. Generics give you one function and keep the types.
Your first generic function
A generic function has a list of type parameters in square brackets, between the name and the regular parameters:
package main
import "fmt"
func Sum[T int | float64](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(Sum([]int{1, 2, 3}))
fmt.Println(Sum([]float64{1.5, 2.5}))
}Code language: Go (go)
6
4Code language: plaintext (plaintext)
Read [T int | float64] as “T is a placeholder for a type, and that type must be int or float64“. Inside the function, T is used like any other type: var total T, nums []T, a return value of type T.
The part after T is the constraint. It says which types are allowed, and therefore what you may do with a T. Here every allowed type supports +, so total += n compiles. When you call Sum with a []int, the compiler checks the call as if you had written the int version by hand.
Constraints decide what you can do
Go only lets you use an operation on T if every type in the constraint supports it. Try to find the largest value with the loosest constraint, any:
package main
import "fmt"
func Largest[T any](s []T) T {
best := s[0]
for _, v := range s[1:] {
if v > best {
best = v
}
}
return best
}
func main() {
fmt.Println(Largest([]int{3, 7, 2}))
}Code language: Go (go)
./main.go:8:6: invalid operation: v > best (type parameter T cannot use operator >)Code language: plaintext (plaintext)
The compiler is right: any includes types like structs and maps that have no order, so > isn’t allowed. You fix it by choosing a constraint that promises ordering. Go has four you’ll use most of the time.
any: no requirements
any allows every type. You can store, pass and return values of type T, but not compare or add them. That’s enough for functions that only move values around:
package main
import (
"fmt"
"strings"
)
func Map[T, U any](s []T, f func(T) U) []U {
out := make([]U, 0, len(s))
for _, v := range s {
out = append(out, f(v))
}
return out
}
func main() {
words := []string{"go", "is", "fun"}
fmt.Println(Map(words, strings.ToUpper))
fmt.Println(Map(words, func(w string) int { return len(w) }))
}Code language: Go (go)
[GO IS FUN]
[2 2 3]Code language: plaintext (plaintext)
Map has two type parameters, T for the input elements and U for the output elements. [T, U any] is short for [T any, U any].
comparable: == and !=
comparable allows every type that supports == and !=: numbers, strings, booleans, pointers, and structs or arrays made of those. Slices, maps and functions are not comparable. It’s the same rule that decides which types can be map keys.
package main
import "fmt"
func Count[T comparable](s []T, target T) int {
n := 0
for _, v := range s {
if v == target {
n++
}
}
return n
}
func main() {
fmt.Println(Count([]string{"a", "b", "a", "c", "a"}, "a"))
fmt.Println(Count([]int{1, 2, 3}, 4))
}Code language: Go (go)
3
0Code language: plaintext (plaintext)
cmp.Ordered: < and >
The cmp package in the standard library defines cmp.Ordered: all integer, floating-point and string types (and named types built on them). Those support <, <=, > and >=, which is exactly what Largest needs:
package main
import (
"cmp"
"fmt"
)
func Largest[T cmp.Ordered](s []T) T {
best := s[0]
for _, v := range s[1:] {
if v > best {
best = v
}
}
return best
}
func main() {
fmt.Println(Largest([]int{3, 7, 2}))
fmt.Println(Largest([]float64{0.5, -1, 0.25}))
fmt.Println(Largest([]string{"pear", "apple", "zucchini"}))
}Code language: Go (go)
7
0.5
zucchiniCode language: plaintext (plaintext)
Before you write a generic helper, check the standard library. slices.Max, slices.Contains, slices.Index, slices.Sort and maps.Keys are all generic, and the built-in min and max work on any ordered type. I wrote Largest to show how it works, but in real code I’d call slices.Max.
Your own constraints, and the ~ symbol
A constraint is an interface. Instead of listing methods, a constraint interface can list types, separated by |. Giving the list a name keeps signatures short:
package main
import "fmt"
type Number interface {
int | int64 | float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
type Celsius float64
func main() {
temps := []Celsius{21.5, 19, 23}
fmt.Println(Sum(temps) / 3)
}Code language: Go (go)
Celsius does not satisfy Number (possibly missing ~ for float64 in Number)Code language: plaintext (plaintext)
Celsius is a new named type whose underlying type is float64, but it is not float64 itself, so it isn’t in the list. The compiler even tells you the fix. A tilde in front of a type, ~float64, means “float64 or any type whose underlying type is float64“:
package main
import "fmt"
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
type Celsius float64
func main() {
temps := []Celsius{21.5, 19, 23}
fmt.Println(Sum(temps) / 3)
fmt.Println(Sum([]int{1, 2, 3}))
}Code language: Go (go)
21.166666666666668
6Code language: plaintext (plaintext)
Notice that Sum(temps) returns a Celsius, not a float64. The type you pass in is the type you get back. cmp.Ordered uses ~ for the same reason, which is why Largest also works on a []Celsius.
An interface that lists types (with | or ~) can only be used as a constraint. You can’t declare a variable of type Number. Interfaces with only methods, like fmt.Stringer, work in both places.
Type inference
So far I never wrote the type argument. When you call Sum([]int{1, 2, 3}), the compiler sees a []int where it expects []T and infers that T is int. You can also pass the type explicitly, in square brackets:
package main
import "fmt"
func Sum[T int | float64](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(Sum([]float64{1, 2}))
fmt.Println(Sum[float64]([]float64{1, 2}))
sumInts := Sum[int]
fmt.Println(sumInts([]int{4, 5}))
}Code language: Go (go)
3
3
9Code language: plaintext (plaintext)
Inference only works from the arguments. If a type parameter doesn’t appear in any parameter, the compiler has nothing to go on:
package main
import "fmt"
func Zero[T any]() T {
var z T
return z
}
func main() {
fmt.Println(Zero())
}Code language: Go (go)
in call to Zero, cannot infer TCode language: plaintext (plaintext)
Write Zero[int]() or Zero[string]() and it compiles. My rule: let the compiler infer when it can, and spell the type out when it can’t or when it makes the code clearer.
Generic types: a Stack[T]
Types can have type parameters too. A stack (last in, first out) is a classic example: the logic is the same whether it holds numbers, strings or structs.
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
}
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last, true
}
func (s *Stack[T]) Len() int {
return len(s.items)
}
func main() {
var words Stack[string]
words.Push("first")
words.Push("second")
fmt.Println(words.Len())
for {
w, ok := words.Pop()
if !ok {
break
}
fmt.Println(w)
}
nums := Stack[int]{}
nums.Push(42)
n, _ := nums.Pop()
fmt.Println(n + 1)
}Code language: Go (go)
2
second
first
43Code language: plaintext (plaintext)
A few things to notice:
Stackon its own isn’t a type you can use.Stack[string]andStack[int]are. When you declare a variable without a value, you must write the type argument, because there is nothing to infer it from.- The methods use the receiver
*Stack[T]. They don’t redeclare the constraint;Trefers to the stack’s type parameter. They use pointer receivers becausePushandPopchange the stack (see Methods). Popneeds to return something when the stack is empty.var zero Tgives the zero value of whateverTis:""for strings,0for ints. The extraboolfollows the same “comma ok” pattern as map lookups.
The compiler checks every use. words.Push(42) fails to compile, because words is a Stack[string]. With a stack of any values, that mistake would only show up at run time.
Generic methods (Go 1.27)
Until Go 1.27, only functions and types could declare type parameters. A method could use its type’s parameters (like T in Stack[T]), but it couldn’t add new ones. Go 1.27 lifts that restriction: a method can now have its own type parameter list, after the method name.
That makes it possible to give a generic list a Map method that returns a list of a different element type:
package main
import (
"fmt"
"strconv"
)
type List[E any] []E
func (l List[E]) Map[R any](f func(E) R) List[R] {
out := make(List[R], 0, len(l))
for _, v := range l {
out = append(out, f(v))
}
return out
}
func main() {
nums := List[int]{1, 2, 3}
labels := nums.Map(strconv.Itoa)
fmt.Printf("%q\n", labels)
halves := nums.Map[float64](func(n int) float64 { return float64(n) / 2 })
fmt.Println(halves)
}Code language: Go (go)
["1" "2" "3"]
[0.5 1 1.5]Code language: plaintext (plaintext)
The receiver List[E] uses the type’s parameter; [R any] belongs to the method alone. Inference works as with functions: nums.Map(strconv.Itoa) figures out that R is string, and you can still write Map[float64] yourself.
There are two limits, and both are about interfaces. An interface method can’t declare type parameters:
package main
type Mapper interface {
Map[R any](f func(int) R) []R
}
func main() {}Code language: Go (go)
./main.go:4:5: interface method must have no type parametersCode language: plaintext (plaintext)
And a generic method never satisfies a regular interface method. If an interface asks for Show(int), a method Show[T any](T) doesn’t count, even though Show[int] would fit. The compiler reports that the type “does not implement” the interface. So generic methods are a convenience for calling code (chaining, keeping helpers next to their type), not a way to satisfy interfaces.
When not to use generics
Generics are a tool for one specific job: the same code working on different types. Most Go code doesn’t need them. I reach for something else in these cases:
- You only have one type. Write the plain function. You can make it generic later if a second type shows up. Don’t guess ahead.
- The types behave differently. If each type needs its own logic, that’s what interfaces and methods are for. A function that takes an
io.Writeror afmt.Stringeralready works with many types and is easier to read than a type parameter constrained by that interface. - The standard library has it.
slices,maps,cmp,minandmaxcover most everyday helpers. - It makes the code harder to read. Three type parameters and a custom constraint to save five lines is a bad trade.
Good fits are containers (stacks, queues, sets, caches) and algorithms over slices and maps that don’t care about the element type. If you want the full picture, the official generics tutorial and the language spec go deeper.
Practice
1. Filter. Write a generic function Filter[T any](s []T, keep func(T) bool) []T that returns a new slice with only the elements for which keep returns true. Use it to keep the even numbers from []int{1, 2, 3, 4, 5, 6} and the words longer than three letters from []string{"go", "gopher", "map", "slice"}.
Show a solution
package main
import "fmt"
func Filter[T any](s []T, keep func(T) bool) []T {
var out []T
for _, v := range s {
if keep(v) {
out = append(out, v)
}
}
return out
}
func main() {
evens := Filter([]int{1, 2, 3, 4, 5, 6}, func(n int) bool {
return n%2 == 0
})
fmt.Println(evens)
long := Filter([]string{"go", "gopher", "map", "slice"}, func(w string) bool {
return len(w) > 3
})
fmt.Println(long)
}Code language: Go (go)
[2 4 6]
[gopher slice]Code language: plaintext (plaintext)2. A set. Build a generic Set[T comparable] type backed by a map[T]struct{}, with methods Add(v T), Has(v T) bool and Len() int, and a constructor NewSet[T comparable]() *Set[T]. Add a few strings (including a duplicate) and print the length and a couple of lookups. Why does T need comparable here?
Show a solution
package main
import "fmt"
type Set[T comparable] struct {
items map[T]struct{}
}
func NewSet[T comparable]() *Set[T] {
return &Set[T]{items: make(map[T]struct{})}
}
func (s *Set[T]) Add(v T) {
s.items[v] = struct{}{}
}
func (s *Set[T]) Has(v T) bool {
_, ok := s.items[v]
return ok
}
func (s *Set[T]) Len() int {
return len(s.items)
}
func main() {
langs := NewSet[string]()
langs.Add("go")
langs.Add("rust")
langs.Add("go")
fmt.Println(langs.Len())
fmt.Println(langs.Has("go"))
fmt.Println(langs.Has("java"))
}Code language: Go (go)
2
true
falseCode language: plaintext (plaintext)
T is used as a map key, and map keys must be comparable. With [T any] the compiler rejects map[T]struct{}. NewSet has no arguments to infer T from, so you call it as NewSet[string]().
Key takeaways
- Type parameters go in square brackets:
func Name[T Constraint](...)andtype Name[T Constraint] struct{...}. - The constraint decides which types are allowed and which operations you can use:
any(none),comparable(==),cmp.Ordered(<,>), or your own type list. ~float64means “any type whose underlying type isfloat64“, so named types likeCelsiusfit.- The compiler infers type arguments from the function arguments; write them out when there’s nothing to infer from.
- Since Go 1.27, methods on concrete types can have their own type parameters, but interface methods can’t, and generic methods don’t satisfy interfaces.
- Use generics for the same logic over different types. Prefer plain functions, interfaces and the
slices/mapspackages otherwise.