Go generics let one function work for many types, with constraints saying which types are allowed. Iterators let a function hand out values one at a time to a for loop, and stop the moment the loop breaks.
Generics and iterators arrived in Go a few years apart, but they belong in one post. Generics let you write a function or a type once and use it with many types. Iterators, built on top of them, let a plain for ... range loop walk over anything you can describe with a function.
This post starts with the problem generics solve, works through constraints and generic types, and then builds iterators from scratch before using the ones in the standard library. Every program below was run on Go 1.26, and its output is pasted from the run.
The problem: the same function, twice
Without generics, a function that adds up numbers has to pick one number type. If you need it for int and for float64, you write it 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}))
}
It prints:
6
4
The two bodies are identical. Only the type changed. Every fix you make to one, you have to remember to make to the other, and a third number type means a third copy.
One function with a type parameter
Since Go 1.18 a function can take a type as a parameter, written in square brackets before the normal 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}))
fmt.Println(Sum[float64]([]float64{0.25, 0.5}))
}
It prints:
6
4
0.75
T is the type parameter. int | float64 is its constraint: the set of types T is allowed to be. Inside the function, T works like any other type name, so var total T declares a zero value of whatever T turns out to be.
In the first two calls you didn’t say what T was. The compiler looked at the argument, saw a []int, and worked out that T must be int. That’s type inference. The third call names the type explicitly, Sum[float64], which is called instantiation. Here it’s redundant, but sometimes it’s the only option.
Explain it like I’m ten
A cookie cutter makes the same shape out of any dough you press it into: chocolate, plain, gingerbread. You don’t need a separate star cutter for each kind of dough.
But it only works on dough. Press it into a stone and nothing happens. The constraint is the label on the cutter that says “works on dough”.
The precise version
A generic function is a template the compiler checks once, against its constraint. At each call site, the type argument is either inferred from the ordinary arguments or given in brackets. The compiler then makes sure that type is in the constraint’s type set. Inside the body you may only do what every type in the set allows: + works on T here because both int and float64 support it.
Where the analogy breaks: a cookie cutter makes one shape from mixed dough, but a generic function never mixes types in one call. Sum on a []int returns an int. You can’t pass it a slice that holds both ints and floats, because Go has no such slice.
When inference can’t work
The compiler infers a type parameter only from the arguments you pass. If T appears only in the result, there is nothing to infer it from:
package main
import "fmt"
func Zero[T any]() T {
var z T
return z
}
func main() {
x := Zero()
fmt.Println(x)
}
The build fails with:
./main.go:11:11: in call to Zero, cannot infer T
Go doesn’t look at how you use the result to guess. You have to name the type:
package main
import "fmt"
func Zero[T any]() T {
var z T
return z
}
func main() {
fmt.Printf("%d %q %v\n", Zero[int](), Zero[string](), Zero[bool]())
}
It prints:
0 "" false
Each instantiation gives the zero value of its own type. You’ll meet the same situation with generic types, where Stack[int]{} has no arguments to infer from either.
Constraints: any, comparable and cmp.Ordered
A constraint is an interface, and it decides which operations the function body may use. The widest one is any, which allows every type and therefore almost no operations. You can’t even compare two any values with ==, because some types, such as slices, can’t be compared:
package main
import "fmt"
func Equal[T any](a, b T) bool {
return a == b
}
func main() {
fmt.Println(Equal(1, 1))
}
The build fails with:
./main.go:6:9: invalid operation: a == b (incomparable types in type set)
Two constraints from the standard library cover the common cases. comparable allows every type that supports == and !=. cmp.Ordered, from the cmp package added in Go 1.21, allows every type that supports < and >: the integers, the floats and strings.
package main
import (
"cmp"
"fmt"
)
func Index[T comparable](items []T, want T) int {
for i, item := range items {
if item == want {
return i
}
}
return -1
}
func Largest[T cmp.Ordered](items []T) T {
best := items[0]
for _, item := range items[1:] {
if item > best {
best = item
}
}
return best
}
func main() {
fmt.Println(Index([]string{"red", "green", "blue"}, "blue"))
fmt.Println(Index([]int{4, 8, 15}, 16))
fmt.Println(Largest([]int{4, 8, 15, 16}))
fmt.Println(Largest([]string{"pear", "apple", "fig"}))
}
It prints:
2
-1
16
pear
Pick the narrowest constraint that lets the body do its job. Index only needs ==, so it takes comparable and works on structs too. Largest needs >, so it takes cmp.Ordered. The standard library already has both jobs as slices.Index and slices.Max, with these same constraints.
Union constraints and what ~ means
You can name your own constraint as an interface that lists types, which is tidier than writing int | float64 on every function. There’s a catch with types you define yourself. This one looks like it should work:
package main
import "fmt"
type Number interface {
int | float64
}
type Celsius float64
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
temps := []Celsius{21.5, 19, 23.5}
fmt.Println(Sum(temps))
}
The build fails with:
./main.go:21:17: Celsius does not satisfy Number (possibly missing ~ for float64 in Number)
Celsius is built on float64, but it’s a different type. A type you declare is distinct from the type it’s made from, even though it holds the same values. The type set int | float64 holds exactly two types, and Celsius isn’t one of them.
The compiler even tells you the fix. A ~ before a type means “this type, or any type whose underlying type is this one”:
package main
import "fmt"
type Number interface {
~int | ~float64
}
type Celsius float64
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
temps := []Celsius{21.5, 19, 23.5}
total := Sum(temps)
fmt.Printf("%v %T\n", total, total)
}
It prints:
64 main.Celsius
~float64 now includes float64, Celsius, and any other type declared as type X float64. The result is still a Celsius, not a plain float64, so you keep the meaning of your type. That’s why cmp.Ordered is written with tildes on every line: it has to accept your type UserID int as well as int.
Generic types: a Stack[T]
Types can take type parameters as well as functions can. A stack is the classic case, because the logic is the same whatever it holds:
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 nums Stack[int]
nums.Push(1)
nums.Push(2)
fmt.Println(nums.Pop())
fmt.Println(nums.Len())
words := Stack[string]{}
w, ok := words.Pop()
fmt.Printf("%q %v\n", w, ok)
words.Push("go")
fmt.Println(words.Pop())
}
It prints:
2 true
1
"" false
go true
Stack[int] and Stack[string] are two separate types. Each method writes Stack[T] in its receiver, and T inside the method is whatever the stack was made with. Pop returns a comma-ok pair, the same pattern maps use, so an empty stack gives 0 false or "" false instead of a panic.
A method can’t declare type parameters of its own, only use the type’s:
package main
import "fmt"
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Map[U any](f func(T) U) []U {
return nil
}
func main() {
fmt.Println(Stack[int]{})
}
The build fails with:
syntax error: method must have no type parameters
When you need that, write a plain function instead: func Map[T, U any](s *Stack[T], f func(T) U) []U.
When not to use generics
Generics are for code that is identical across types and only moves the values around: containers, Sum, Index, Largest. When the code calls methods on the value, an interface is usually simpler.
Take a function that prints anything with a String method. You could write func Show[T fmt.Stringer](v T). But func Show(v fmt.Stringer) does the same job, reads more plainly, and was the normal Go way long before generics existed. The type parameter adds nothing, because the body only calls v.String().
A rough guide:
- If you’d write the same body for several types and only the type name changes, use a type parameter.
- If each type does its own thing behind a method, use an interface.
- If you have one type today, use that type. You can make it generic when a second one shows up.
Iterators: a function that hands out values
Since Go 1.23 a for ... range loop can range over a function. Such a function is called an iterator. It takes one argument, a callback conventionally named yield, and calls it once for each value:
package main
import (
"fmt"
"iter"
)
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
if !yield(i) {
return
}
}
}
}
func main() {
for n := range Countdown(3) {
fmt.Println(n)
}
fmt.Println("liftoff")
}
It prints:
3
2
1
liftoff
iter.Seq[int] is a generic type from the iter package. It’s just a name for func(yield func(int) bool). Countdown doesn’t count anything itself. It returns a function that counts when the loop asks it to.
Each time the iterator calls yield(i), the loop body runs once with n set to i. When the iterator function returns, the loop ends.
Explain it like I’m ten
An iterator is a vending machine. Each time you press the button, it hands you one item. You don’t get the whole stock dumped on the floor at once, and the machine doesn’t need to know how many you want.
You can also walk away whenever you like. The machine notices nobody is pressing the button and stops handing things out.
The precise version
The loop body becomes the yield function. yield returns true if the loop wants another value, and false if the loop has finished early, through break, return or a goto out of the loop. The iterator must check that bool and return as soon as it sees false. The range loop runs until the iterator function returns.
Where the analogy breaks: with a real vending machine, you press the button and pull the next item out when you’re ready. A Go iterator works the other way round. The machine is in charge. It calls you, your loop body, once per item, and you only get to answer “more” or “stop”. That’s a push, not a pull, and it’s why the iter.Pull function exists, as the last section shows.
break makes yield return false
Watching the iterator itself shows what false does. Here it prints a line before each value, and another when it stops:
package main
import (
"fmt"
"iter"
)
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
fmt.Println("iterator: sending", i)
if !yield(i) {
fmt.Println("iterator: loop said stop")
return
}
}
fmt.Println("iterator: ran out")
}
}
func main() {
for n := range Countdown(5) {
fmt.Println("loop: got", n)
if n == 4 {
break
}
}
fmt.Println("after the loop")
}
It prints:
iterator: sending 5
loop: got 5
iterator: sending 4
loop: got 4
iterator: loop said stop
after the loop
The iterator sent 5 and got true back. It sent 4, and the loop body hit break, so that yield call returned false. The iterator printed its message and returned, and only then did after the loop run. It never computed 3, 2 or 1.
If an iterator ignores the false and keeps calling yield, Go doesn’t quietly run your loop body again. It panics:
package main
import "fmt"
func Countdown(n int) func(func(int) bool) {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
yield(i)
}
}
}
func main() {
for n := range Countdown(3) {
fmt.Println(n)
if n == 2 {
break
}
}
}
It prints two lines, then stops:
3
2
panic: runtime error: range function continued iteration after function for loop body returned false
This version also shows that iter.Seq is only a name. A plain func(func(int) bool) works in a range loop just the same.
Iterators that take iterators
Because an iterator is a value, a function can take one and return another. Filter passes on only the values that pass a test:
package main
import (
"fmt"
"iter"
)
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
if !yield(i) {
return
}
}
}
}
func Filter[T any](seq iter.Seq[T], keep func(T) bool) iter.Seq[T] {
return func(yield func(T) bool) {
for v := range seq {
if keep(v) && !yield(v) {
return
}
}
}
}
func main() {
even := func(n int) bool { return n%2 == 0 }
for n := range Filter(Countdown(10), even) {
fmt.Println(n)
}
}
It prints:
10
8
6
4
2
Filter is generic, so it works on a sequence of anything. Inside, it ranges over the input sequence with an ordinary loop. When its own caller stops, yield returns false, Filter returns, and that ends its inner loop too, which in turn makes Countdown‘s yield return false. A break at the top travels all the way down the chain.
Two values per step: iter.Seq2
Some sequences naturally hand out pairs, such as an index and a value, or a key and a value. For those, iter.Seq2[K, V] is func(yield func(K, V) bool), and the range loop gets two variables. The slices and maps packages return both kinds:
package main
import (
"fmt"
"maps"
"slices"
"strings"
)
func main() {
fruits := []string{"pear", "apple", "fig"}
for i, f := range slices.All(fruits) {
fmt.Println(i, f)
}
for f := range slices.Values(fruits) {
fmt.Println(strings.ToUpper(f))
}
stock := map[string]int{"pear": 3, "apple": 0, "fig": 12}
names := slices.Collect(maps.Keys(stock))
fmt.Println(len(names))
fmt.Println(slices.Sorted(maps.Keys(stock)))
}
It prints:
0 pear
1 apple
2 fig
PEAR
APPLE
FIG
3
[apple fig pear]
slices.All is an iter.Seq2[int, string]: index and value, like ranging over the slice itself. slices.Values is an iter.Seq[string] with just the values. maps.Keys is an iter.Seq[string] over the keys, in the map’s usual unpredictable order, which is why the program prints only how many keys slices.Collect gathered and not the slice itself.
That last line is the one you’ve seen in the parts on control flow and maps. Now you can read it fully. maps.Keys(stock) doesn’t build a slice. It returns an iterator. slices.Sorted ranges over that iterator, collects every value into a new slice, sorts it, and returns it. slices.Sorted takes an iter.Seq[E] where E is cmp.Ordered, so both halves of this post meet in one line.
Ranging over a slice or map directly is still the normal way. These functions earn their place when you want to pass a sequence to something else, like slices.Sorted, or to your own Filter.
Pulling values with iter.Pull
Sometimes you do want to press the button yourself, one value at a time, outside a loop. iter.Pull turns a push iterator into a next function and a stop function:
package main
import (
"fmt"
"iter"
)
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
if !yield(i) {
return
}
}
}
}
func main() {
next, stop := iter.Pull(Countdown(2))
defer stop()
for range 3 {
v, ok := next()
fmt.Println(v, ok)
}
}
It prints:
2 true
1 true
0 false
Each call to next returns the next value and true, or the zero value and false once the iterator is done. The third call gets 0 false, the same comma-ok shape as Stack.Pop.
Calling stop tells the iterator you’re finished, so its yield returns false and it can clean up. defer stop() makes sure that happens even if you stop pulling early. You’d reach for iter.Pull when you need to step through two sequences side by side, for example to compare them, which a single range loop can’t do.
What to remember
- A type parameter lets you write a function or type once for many types. The compiler usually infers it from the arguments. When it can’t, write it in brackets:
Zero[int](). - A constraint is the set of types allowed, and it decides what the body may do. Use
any,comparableorcmp.Orderedbefore inventing your own. ~float64means any type whose underlying type isfloat64. Without the tilde, yourtype Celsius float64is left out.- Methods can’t have their own type parameters. When the body calls methods on a value, an interface is often the simpler choice.
- An iterator is a function that calls
yieldonce per value. Whenyieldreturnsfalse, the loop has stopped, and the iterator must return. maps.Keysandslices.Allreturn iterators, andslices.Collectandslices.Sortedturn an iterator back into a slice.
An iterator doesn’t hand you a list. It calls you once for each value, until you say stop.