Go 1.24 Iterator Advanced: range over func Deep Practice 2026
Go 1.24 Iterator Advanced: range over func Deep Practice
Go 1.22 introduced range over int, Go 1.23 introduced range over func — Go finally has "first-class" iterators. But many Gophers are still stuck at for i, v := range slice, with their understanding of range over func limited to the standard library's slices and maps packages.
By Go 1.24, the iterator ecosystem has matured: generic iterators, iterator composition, lazy evaluation pipelines... these patterns can make your Go code more elegant, more efficient, and more Go-idiomatic. This article walks through 5 core patterns to help you fully master Go iterators.
Core Concepts at a Glance
| Concept | Description | Signature |
|---|---|---|
| Iterator Function | Function type that can be ranged over | func(yield func(V) bool) |
| Indexed Iterator | Returns both index and value | func(yield func(int, V) bool) |
| Pull Iterator | Manually pull next value | func() (V, bool) |
| Push Iterator | Push values to yield callback | func(yield func(V) bool) |
| Iterator Composition | Chain/merge multiple iterators | func(...Iter) Iter |
| Lazy Evaluation | Deferred computation until consumption | Iterators are naturally lazy |
5 Major Pain Points of Go Iterators
- Verbose custom collection traversal: Trees, graphs, linked lists require hand-written traversal callbacks
- Missing pipeline-style data processing: Can't chain filter/map like Python generators
- Complex generic iterator types:
func(yield func(V) bool)signature confuses beginners - Iterator-goroutine interaction: Correct usage patterns for concurrent iterators are unclear
- Pull vs Push confusion: When to use which iterator mode is ambiguous
Pattern 1: range over func Basics
The three iterator function signatures introduced in Go 1.23 are the foundation for understanding everything.
// go-iterator-basics/main.go
// Environment: Go 1.24+ / No extra dependencies
package main
import (
"fmt"
"iter"
)
// Range generates an integer sequence iterator
func Range(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := range n {
if !yield(i) {
return
}
}
}
}
// RangeWithStep generates an integer sequence with step
func RangeWithStep(start, end, step int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := start; i < end; i += step {
if !yield(i) {
return
}
}
}
}
// Enumerate adds indices to any slice
func Enumerate[T any](items []T) iter.Seq2[int, T] {
return func(yield func(int, T) bool) {
for i, item := range items {
if !yield(i, item) {
return
}
}
}
}
// Take only the first n elements
func Take[T any](n int, seq iter.Seq[T]) iter.Seq[T] {
return func(yield func(T) bool) {
count := 0
for v := range seq {
if count >= n {
return
}
if !yield(v) {
return
}
count++
}
}
}
// ToPull converts a Push iterator to a Pull iterator
func ToPull[T any](seq iter.Seq[T]) func() (T, bool) {
next, stop := iter.Pull(seq)
return func() (T, bool) {
v, ok := next()
return v, ok
}
}
func main() {
fmt.Println("=== Basic Iterator ===")
for i := range Range(5) {
fmt.Print(i, " ") // 0 1 2 3 4
}
fmt.Println()
fmt.Println("=== With Step ===")
for i := range RangeWithStep(0, 20, 5) {
fmt.Print(i, " ") // 0 5 10 15
}
fmt.Println()
fmt.Println("=== With Index ===")
fruits := []string{"Apple", "Banana", "Cherry"}
for i, fruit := range Enumerate(fruits) {
fmt.Printf("%d: %s ", i, fruit)
}
fmt.Println()
fmt.Println("=== Early Exit ===")
for v := range Take(3, Range(100)) {
fmt.Print(v, " ") // 0 1 2
}
fmt.Println()
fmt.Println("=== Pull Iterator ===")
pull := ToPull(Range(3))
for {
v, ok := pull()
if !ok {
break
}
fmt.Print(v, " ") // 0 1 2
}
fmt.Println()
}
Pattern 2: Generic Iterators
Generics make iterators true "first-class citizens" — a single Filter function can filter sequences of any type.
// go-generic-iterator/main.go
// Environment: Go 1.24+ / No extra dependencies
package main
import (
"fmt"
"iter"
"strings"
)
// Filter elements in a sequence
func Filter[T any](seq iter.Seq[T], predicate func(T) bool) iter.Seq[T] {
return func(yield func(T) bool) {
for v := range seq {
if predicate(v) {
if !yield(v) {
return
}
}
}
}
}
// Map transforms each element in a sequence
func Map[T any, R any](seq iter.Seq[T], transform func(T) R) iter.Seq[R] {
return func(yield func(R) bool) {
for v := range seq {
if !yield(transform(v)) {
return
}
}
}
}
// FlatMap transforms and flattens each element
func FlatMap[T any, R any](seq iter.Seq[T], transform func(T) iter.Seq[R]) iter.Seq[R] {
return func(yield func(R) bool) {
for v := range seq {
for r := range transform(v) {
if !yield(r) {
return
}
}
}
}
}
// Reduce a sequence to a single value
func Reduce[T any, R any](seq iter.Seq[T], initial R, accumulator func(R, T) R) R {
result := initial
for v := range seq {
result = accumulator(result, v)
}
return result
}
// Chunk a sequence into fixed-size groups
func Chunk[T any](size int, seq iter.Seq[T]) iter.Seq[[]T] {
return func(yield func([]T) bool) {
chunk := make([]T, 0, size)
for v := range seq {
chunk = append(chunk, v)
if len(chunk) == size {
if !yield(chunk) {
return
}
chunk = make([]T, 0, size)
}
}
if len(chunk) > 0 {
yield(chunk)
}
}
}
// Distinct removes duplicate elements
func Distinct[T comparable](seq iter.Seq[T]) iter.Seq[T] {
return func(yield func(T) bool) {
seen := make(map[T]bool)
for v := range seq {
if !seen[v] {
seen[v] = true
if !yield(v) {
return
}
}
}
}
}
func main() {
numbers := slicesToIter([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
// Chain: filter evens → square → take first 3
evens := Filter(numbers, func(n int) bool { return n%2 == 0 })
squares := Map(evens, func(n int) int { return n * n })
first3 := Take(3, squares)
fmt.Print("Chain: ")
for v := range first3 {
fmt.Print(v, " ") // 4 16 36
}
fmt.Println()
sum := Reduce(slicesToIter([]int{1, 2, 3, 4, 5}), 0,
func(acc, n int) int { return acc + n })
fmt.Println("Sum:", sum) // 15
fmt.Print("Chunked: ")
for chunk := range Chunk(3, slicesToIter([]int{1, 2, 3, 4, 5, 6, 7})) {
fmt.Print(chunk, " ")
}
fmt.Println()
fmt.Print("Distinct: ")
for v := range Distinct(slicesToIter([]string{"go", "rust", "go", "python", "rust"})) {
fmt.Print(v, " ")
}
fmt.Println()
// FlatMap
sentences := slicesToIter([]string{"hello world", "go is great"})
words := FlatMap(sentences, func(s string) iter.Seq[string] {
return slicesToIter(strings.Split(s, " "))
})
fmt.Print("FlatMap: ")
for w := range words {
fmt.Print(w, " ") // hello world go is great
}
fmt.Println()
}
func slicesToIter[T any](s []T) iter.Seq[T] {
return func(yield func(T) bool) {
for _, v := range s {
if !yield(v) {
return
}
}
}
}
Pattern 3: Tree/Graph Traversal Iterators
The most powerful application of iterators — turning recursive traversal into composable pipelines.
// go-tree-iterator/main.go
// Environment: Go 1.24+ / No extra dependencies
package main
import (
"fmt"
"iter"
)
type TreeNode[T any] struct {
Value T
Left *TreeNode[T]
Right *TreeNode[T]
}
// InOrder traversal iterator (Left-Root-Right)
func (n *TreeNode[T]) InOrder() iter.Seq[T] {
return func(yield func(T) bool) {
n.inOrderHelper(yield)
}
}
func (n *TreeNode[T]) inOrderHelper(yield func(T) bool) bool {
if n == nil {
return true
}
if !n.Left.inOrderHelper(yield) {
return false
}
if !yield(n.Value) {
return false
}
if !n.Right.inOrderHelper(yield) {
return false
}
return true
}
// PreOrder traversal iterator (Root-Left-Right)
func (n *TreeNode[T]) PreOrder() iter.Seq[T] {
return func(yield func(T) bool) {
n.preOrderHelper(yield)
}
}
func (n *TreeNode[T]) preOrderHelper(yield func(T) bool) bool {
if n == nil {
return true
}
if !yield(n.Value) {
return false
}
if !n.Left.preOrderHelper(yield) {
return false
}
if !n.Right.preOrderHelper(yield) {
return false
}
return true
}
// LevelOrder traversal iterator (BFS)
func (n *TreeNode[T]) LevelOrder() iter.Seq[T] {
return func(yield func(T) bool) {
if n == nil {
return
}
queue := []*TreeNode[T]{n}
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
if !yield(current.Value) {
return
}
if current.Left != nil {
queue = append(queue, current.Left)
}
if current.Right != nil {
queue = append(queue, current.Right)
}
}
}
}
// Graph definition
type Graph[T comparable] struct {
adjList map[T][]T
}
func NewGraph[T comparable]() *Graph[T] {
return &Graph[T]{adjList: make(map[T][]T)}
}
func (g *Graph[T]) AddEdge(from, to T) {
g.adjList[from] = append(g.adjList[from], to)
}
// DFS depth-first traversal iterator
func (g *Graph[T]) DFS(start T) iter.Seq[T] {
return func(yield func(T) bool) {
visited := make(map[T]bool)
g.dfsHelper(start, visited, yield)
}
}
func (g *Graph[T]) dfsHelper(node T, visited map[T]bool, yield func(T) bool) bool {
if visited[node] {
return true
}
visited[node] = true
if !yield(node) {
return false
}
for _, neighbor := range g.adjList[node] {
if !g.dfsHelper(neighbor, visited, yield) {
return false
}
}
return true
}
// BFS breadth-first traversal iterator
func (g *Graph[T]) BFS(start T) iter.Seq[T] {
return func(yield func(T) bool) {
visited := make(map[T]bool)
queue := []T{start}
visited[start] = true
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
if !yield(node) {
return
}
for _, neighbor := range g.adjList[node] {
if !visited[neighbor] {
visited[neighbor] = true
queue = append(queue, neighbor)
}
}
}
}
}
func main() {
root := &TreeNode[int]{
Value: 4,
Left: &TreeNode[int]{Value: 2, Left: &TreeNode[int]{Value: 1}, Right: &TreeNode[int]{Value: 3}},
Right: &TreeNode[int]{Value: 6, Left: &TreeNode[int]{Value: 5}, Right: &TreeNode[int]{Value: 7}},
}
fmt.Print("InOrder: ")
for v := range root.InOrder() { fmt.Print(v, " ") } // 1 2 3 4 5 6 7
fmt.Println()
fmt.Print("PreOrder: ")
for v := range root.PreOrder() { fmt.Print(v, " ") } // 4 2 1 3 6 5 7
fmt.Println()
fmt.Print("LevelOrder: ")
for v := range root.LevelOrder() { fmt.Print(v, " ") } // 4 2 6 1 3 5 7
fmt.Println()
g := NewGraph[string]()
g.AddEdge("A", "B")
g.AddEdge("A", "C")
g.AddEdge("B", "D")
g.AddEdge("C", "D")
g.AddEdge("D", "E")
fmt.Print("DFS: ")
for v := range g.DFS("A") { fmt.Print(v, " ") } // A B D E C
fmt.Println()
fmt.Print("BFS: ")
for v := range g.BFS("A") { fmt.Print(v, " ") } // A B C D E
fmt.Println()
}
Pattern 4: Iterator Composition and Pipelines
The most elegant use of iterators — composing data processing flows like Unix pipes.
// go-iterator-pipeline/main.go
// Environment: Go 1.24+ / No extra dependencies
package main
import (
"fmt"
"iter"
"strings"
)
// Concat concatenates multiple iterators
func Concat[T any](seqs ...iter.Seq[T]) iter.Seq[T] {
return func(yield func(T) bool) {
for _, seq := range seqs {
for v := range seq {
if !yield(v) { return }
}
}
}
}
// Interleave alternates elements from multiple iterators
func Interleave[T any](seqs ...iter.Seq[T]) iter.Seq[T] {
return func(yield func(T) bool) {
pulls := make([]func() (T, bool), len(seqs))
stops := make([]func(), len(seqs))
for i, seq := range seqs {
pulls[i], stops[i] = iter.Pull(seq)
}
defer func() { for _, stop := range stops { stop() } }()
active := len(seqs)
for active > 0 {
for i, pull := range pulls {
v, ok := pull()
if !ok { pulls[i] = nil; active--; continue }
if !yield(v) { return }
}
}
}
}
// TakeWhile takes values while predicate is true
func TakeWhile[T any](seq iter.Seq[T], predicate func(T) bool) iter.Seq[T] {
return func(yield func(T) bool) {
for v := range seq {
if !predicate(v) { return }
if !yield(v) { return }
}
}
}
// DropWhile skips values while predicate is true
func DropWhile[T any](seq iter.Seq[T], predicate func(T) bool) iter.Seq[T] {
return func(yield func(T) bool) {
dropping := true
for v := range seq {
if dropping && predicate(v) { continue }
dropping = false
if !yield(v) { return }
}
}
}
// Log processing pipeline
type LogEntry struct {
Timestamp string
Level string
Message string
}
func ParseLogs(rawLines iter.Seq[string]) iter.Seq[LogEntry] {
return Map(rawLines, func(line string) LogEntry {
parts := strings.SplitN(line, " ", 3)
if len(parts) < 3 { return LogEntry{Message: line} }
return LogEntry{Timestamp: parts[0], Level: parts[1], Message: parts[2]}
})
}
func FilterByLevel(level string, logs iter.Seq[LogEntry]) iter.Seq[LogEntry] {
return Filter(logs, func(l LogEntry) bool { return l.Level == level })
}
func ExtractMessages(logs iter.Seq[LogEntry]) iter.Seq[string] {
return Map(logs, func(l LogEntry) string { return l.Message })
}
func main() {
fmt.Print("Concat: ")
for v := range Concat(slicesToIter([]int{1, 2, 3}), slicesToIter([]int{4, 5, 6})) {
fmt.Print(v, " ") // 1 2 3 4 5 6
}
fmt.Println()
fmt.Print("Interleave: ")
for v := range Interleave(slicesToIter([]string{"A", "B", "C"}), slicesToIter([]string{"1", "2", "3"})) {
fmt.Print(v, " ") // A 1 B 2 C 3
}
fmt.Println()
// Log pipeline
rawLogs := slicesToIter([]string{
"2026-01-01 ERROR Database connection failed",
"2026-01-01 INFO Service started",
"2026-01-01 ERROR Cache timeout",
"2026-01-01 WARN High memory usage",
})
fmt.Println("\n=== Log Pipeline: ERROR messages only ===")
for msg := range ExtractMessages(FilterByLevel("ERROR", ParseLogs(rawLogs))) {
fmt.Println(" ", msg)
}
}
func slicesToIter[T any](s []T) iter.Seq[T] {
return func(yield func(T) bool) {
for _, v := range s { if !yield(v) { return } }
}
}
func Filter[T any](seq iter.Seq[T], predicate func(T) bool) iter.Seq[T] {
return func(yield func(T) bool) {
for v := range seq {
if predicate(v) { if !yield(v) { return } }
}
}
}
func Map[T any, R any](seq iter.Seq[T], transform func(T) R) iter.Seq[R] {
return func(yield func(R) bool) {
for v := range seq { if !yield(transform(v)) { return } }
}
}
func Take[T any](n int, seq iter.Seq[T]) iter.Seq[T] {
return func(yield func(T) bool) {
count := 0
for v := range seq {
if count >= n { return }
if !yield(v) { return }
count++
}
}
}
Pattern 5: Production-Grade Iterator Patterns
Applying iterators to real production scenarios: database pagination, file stream processing, concurrent iteration.
// go-production-iterator/main.go
// Environment: Go 1.24+ / No extra dependencies
package main
import (
"bufio"
"fmt"
"iter"
"strings"
)
// Pagination iterator
type Page[T any] struct {
Items []T
PageNum int
HasMore bool
}
func Paginate[T any](fetchPage func(pageNum int) (Page[T], error)) iter.Seq2[int, T] {
return func(yield func(int, T) bool) {
pageNum := 0
for {
page, err := fetchPage(pageNum)
if err != nil { return }
for _, item := range page.Items {
if !yield(pageNum, item) { return }
}
if !page.HasMore { return }
pageNum++
}
}
}
// File line iterator
func LinesFromString(s string) iter.Seq[string] {
return func(yield func(string) bool) {
scanner := bufio.NewScanner(strings.NewReader(s))
for scanner.Scan() {
if !yield(scanner.Text()) { return }
}
}
}
// Sliding window iterator
func SlidingWindow[T any](size int, seq iter.Seq[T]) iter.Seq[[]T] {
return func(yield func([]T) bool) {
window := make([]T, 0, size)
for v := range seq {
window = append(window, v)
if len(window) > size { window = window[1:] }
if len(window) == size {
snapshot := make([]T, size)
copy(snapshot, window)
if !yield(snapshot) { return }
}
}
}
}
// Deduplicate sorted sequence (no extra memory)
func Deduplicate[T comparable](seq iter.Seq[T]) iter.Seq[T] {
return func(yield func(T) bool) {
var prev T
first := true
for v := range seq {
if first || v != prev {
first = false
prev = v
if !yield(v) { return }
}
}
}
}
func main() {
fmt.Println("=== Pagination Iterator ===")
mockFetchPage := func(pageNum int) (Page[string], error) {
allItems := [][]string{{"User1", "User2", "User3"}, {"User4", "User5"}, {"User6"}}
if pageNum >= len(allItems) { return Page[string]{HasMore: false}, nil }
return Page[string]{Items: allItems[pageNum], PageNum: pageNum, HasMore: pageNum < len(allItems)-1}, nil
}
for pageNum, user := range Paginate(mockFetchPage) {
fmt.Printf(" Page %d: %s\n", pageNum, user)
}
fmt.Println("\n=== Sliding Window ===")
for window := range SlidingWindow(3, slicesToIter([]int{1, 2, 3, 4, 5, 6, 7})) {
fmt.Print(window, " ") // [1 2 3] [2 3 4] [3 4 5] [4 5 6] [5 6 7]
}
fmt.Println()
fmt.Print("Deduplicate: ")
for v := range Deduplicate(slicesToIter([]int{1, 1, 2, 2, 3, 3, 3, 4})) {
fmt.Print(v, " ") // 1 2 3 4
}
fmt.Println()
}
func slicesToIter[T any](s []T) iter.Seq[T] {
return func(yield func(T) bool) {
for _, v := range s { if !yield(v) { return } }
}
}
Pitfall Guide: 5 Common Traps
Trap 1: Modifying Underlying Collection During Iteration
// ❌ Wrong: Modifying slice during iteration
for i, v := range mySlice {
mySlice = append(mySlice, v) // Dangerous!
}
// ✅ Right: Copy first, then iterate
copied := make([]int, len(mySlice))
copy(copied, mySlice)
for v := range slicesToIter(copied) { /* safe */ }
Trap 2: Forgetting to Call stop on Pull Iterators
// ❌ Wrong: stop is ignored
next, _ := iter.Pull(mySeq)
// ✅ Right: Always defer stop
next, stop := iter.Pull(mySeq)
defer stop()
Trap 3: Reusing Iterators Causes Unexpected Behavior
// ❌ Wrong: Iterators are consumable
seq := Range(5)
for v := range seq { fmt.Print(v) } // 0 1 2 3 4
for v := range seq { fmt.Print(v) } // No output!
// ✅ Right: Create new iterator each time
for v := range Range(5) { fmt.Print(v) }
for v := range Range(5) { fmt.Print(v) }
Trap 4: Recursive Iterator Stack Overflow
Deep recursive trees may cause stack overflow. Use explicit stack (level-order style) instead.
Trap 5: Goroutine Leaks
When a consumer exits early from a Buffered iterator, ensure the done channel can notify the producer to stop.
Error Troubleshooting Table
| Error Message | Cause | Solution |
|---|---|---|
cannot range over xxx |
Type is not iter.Seq/iter.Seq2 | Verify function signature is func(yield func(V) bool) |
yield is not used |
yield not called in iterator function | Ensure yield(v) is called in loop |
cannot use seq as iter.Seq |
Iterator signature mismatch | Check iter.Seq[T] vs iter.Seq2[K,V] |
panic: range over nil |
Iterator function is nil | Add nil check, return empty iterator |
goroutine leak |
Pull iterator stop not called | defer stop() |
stack overflow |
Recursive iterator too deep | Use explicit stack iteration |
deadlock |
Iterator internal channel blocked | Add context cancellation or timeout |
unexpected address |
yield returns a copy | Go value semantics; use *T for pointers |
iterator consumed twice |
Iterator can only be consumed once | Create new iterator each time |
invalid memory address |
Accessing closed resource in iterator | Ensure resources close after iteration completes |
Advanced Optimization: 5 Production-Grade Tips
Tip 1: Iterator + Context Integration
func ContextAware[T any](ctx context.Context, seq iter.Seq[T]) iter.Seq[T] {
return func(yield func(T) bool) {
next, stop := iter.Pull(seq)
defer stop()
for {
select {
case <-ctx.Done():
return
default:
v, ok := next()
if !ok { return }
if !yield(v) { return }
}
}
}
}
Tip 2: Parallel Iterators
Implement ParallelMap using goroutines and channels for concurrent element processing.
Tip 3: Iterator Performance Benchmarks
Use Go benchmarks to compare iterator vs traditional for-loop performance.
Tip 4: Custom Collections Implementing iter Interface
Implement All(), Keys(), Values() iterator methods for custom collections like OrderedMap.
Tip 5: Iterator Middleware Pattern
func WithMetrics[T any](name string, seq iter.Seq[T]) iter.Seq[T] {
return func(yield func(T) bool) {
count := 0
start := time.Now()
for v := range seq {
count++
if !yield(v) {
log.Printf("[ITER:%s] interrupted after %d items", name, count)
return
}
}
log.Printf("[ITER:%s] completed %d items, elapsed: %v", name, count, time.Since(start))
}
}
Go Iterators vs Other Languages
| Dimension | Go 1.24 iter | Python Generator | Rust Iterator | Java Stream |
|---|---|---|---|---|
| Type | func(yield func(V) bool) |
yield v |
trait Iterator |
Stream<T> |
| Generics | ✅ Full generics | ❌ Dynamic typing | ✅ Full generics | ✅ Full generics |
| Lazy Evaluation | ✅ Naturally lazy | ✅ Naturally lazy | ✅ Naturally lazy | ✅ Pull/push optional |
| Early Exit | ✅ yield returns false | ✅ break | ✅ take/scan | ⚠️ Needs short-circuit ops |
| Composability | ✅ Function composition | ✅ Generator composition | ✅ Adapter composition | ✅ Stream operations |
| Concurrency Safe | ❌ Not safe | ❌ GIL limited | ❌ Not safe | ✅ Parallel streams |
| Performance Overhead | Low (function call) | High (interpreted) | Zero-cost abstraction | Medium (boxing/unboxing) |
| Learning Curve | Medium | Low | Medium-High | Medium |
| Ecosystem Maturity | New (1.23) | Mature | Mature | Mature |
Conclusion
Go 1.24's range over func iterators are a significant boost to Go's expressiveness:
- Basics: Understand
iter.Seq[T]anditer.Seq2[K,V]signatures, master yield-returning-false early exit - Generic iterators: Filter/Map/Reduce/Chunk make data processing elegant as pipelines
- Tree/Graph traversal: Encapsulate recursive traversal as composable, interruptible, reusable iterators
- Iterator pipelines: Concat/Interleave/TakeWhile/DropWhile for data flow composition
- Production patterns: Pagination iterators, file streams, sliding windows, context-aware cancellation
Key principles: iterators are consumable, not concurrency-safe, and naturally lazy. Understanding these three points helps you avoid 90% of pitfalls.
Recommended Online Tools
- /en/json/format - JSON formatter for iterator-returned JSON data
- /en/dev/curl-to-code - cURL to code generator for Go HTTP iterators
- /en/encode/hash - Hash calculator for iterator deduplication and verification
- /en/text/diff - Text diff for comparing iterator version changes
Try these browser-local tools — no sign-up required →