目录

Go 1.23 新特性速览:range-over-func 终于来了

Go 1.23 发布于 2024 年 8 月,带来了 range-over-func 这个期待已久的特性。六个月后回头看,哪些特性真正改变了日常开发?这篇文章是一次实际使用后的回顾,而不是发布时的功能清单。

range-over-func:改变迭代的方式

它解决了什么问题

Go 一直有个痛点:自定义集合类型无法参与 for range 循环。如果你写了一个链表、树或者惰性序列,用户只能用 for i := 0; i < coll.Len(); i++ 或者自己实现 iterator 方法——没有统一的惯用法。

Go 1.23 之前,如果想做惰性迭代,通常借助 channel:

// 旧方式:用 channel 做惰性迭代,有 goroutine 泄漏风险
func integers(start, end int) <-chan int {
    ch := make(chan int)
    go func() {
        for i := start; i < end; i++ {
            ch <- i
        }
        close(ch)
    }()
    return ch
}

// 调用方如果 break 了,goroutine 会永远阻塞
for v := range integers(0, 1000000) {
    if v > 10 {
        break  // goroutine 泄漏!
    }
}

range-over-func 的实现

Go 1.23 引入了 range-over-func:函数签名为 func(yield func(V) bool) 的函数可以直接用于 for rangeyield 是回调函数,返回 false 表示调用方提前退出(break)。

// 实现一个惰性整数序列
func Integers(start, end int) func(yield func(int) bool) {
    return func(yield func(int) bool) {
        for i := start; i < end; i++ {
            if !yield(i) {
                return  // 调用方 break 了,干净退出,没有 goroutine 泄漏
            }
        }
    }
}

func main() {
    for v := range Integers(0, 1000000) {
        fmt.Println(v)
        if v > 10 {
            break  // 完全安全
        }
    }
}

实际应用:自定义树的迭代

type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}

// 实现中序遍历迭代器
func (n *TreeNode) InOrder() func(yield func(int) bool) {
    return func(yield func(int) bool) {
        var traverse func(*TreeNode) bool
        traverse = func(node *TreeNode) bool {
            if node == nil {
                return true
            }
            if !traverse(node.Left) {
                return false
            }
            if !yield(node.Val) {
                return false
            }
            return traverse(node.Right)
        }
        traverse(n)
    }
}

// 调用方代码非常简洁
root := buildTree()
for val := range root.InOrder() {
    fmt.Println(val)
    if val > 100 {
        break  // 树的遍历会干净地停止
    }
}

让自定义集合的迭代有了统一的惯用法,调用方代码更简洁,实现方不用担心 goroutine 泄漏。详见 Go 1.23 Range Spec

iter 包:标准迭代器原语

与 range-over-func 一起引入的是 iter 包,定义了两个核心类型:

// iter.Seq:单值迭代器
type Seq[V any] func(yield func(V) bool)

// iter.Seq2:键值对迭代器(类似 map 迭代)
type Seq2[K, V any] func(yield func(K, V) bool)

标准库同步更新,slicesmaps 包提供了配套函数:

import (
    "iter"
    "maps"
    "slices"
)

// slices.All 返回 iter.Seq2[int, T]
for i, v := range slices.All([]string{"a", "b", "c"}) {
    fmt.Println(i, v)  // 0 a, 1 b, 2 c
}

// slices.Values 返回 iter.Seq[T]
for v := range slices.Values([]int{1, 2, 3}) {
    fmt.Println(v)
}

// maps.Keys 和 maps.All
for k := range maps.Keys(myMap) {
    fmt.Println(k)
}

迭代器组合

iter 包的价值在于组合。可以把过滤、变换等操作写成迭代器,然后链式使用:

func Filter[V any](seq iter.Seq[V], f func(V) bool) iter.Seq[V] {
    return func(yield func(V) bool) {
        for v := range seq {
            if f(v) {
                if !yield(v) {
                    return
                }
            }
        }
    }
}

func Map[V, W any](seq iter.Seq[V], f func(V) W) iter.Seq[W] {
    return func(yield func(W) bool) {
        for v := range seq {
            if !yield(f(v)) {
                return
            }
        }
    }
}

// 处理大文件,按行过滤和转换,不把全部内容加载进内存
lines := readLines("huge-file.txt")  // 返回 iter.Seq[string]
results := Map(
    Filter(lines, func(l string) bool { return strings.Contains(l, "ERROR") }),
    strings.TrimSpace,
)
for line := range results {
    process(line)
}

惰性求值,每次只处理一个元素,内存占用固定。

Timer 改进:不再泄漏 goroutine

这是 Go 1.23 里最容易被忽视的改进,但实际影响很大。

旧问题:time.After 的内存泄漏

// 有问题的旧代码
func processWithTimeout(items []Item) {
    for _, item := range items {
        select {
        case result := <-process(item):
            handle(result)
        case <-time.After(5 * time.Second):
            // time.After 创建的 timer 要等 5 秒触发后才会被 GC
            // 高频循环里会积累大量未 GC 的 timer,造成内存压力
            log.Println("timeout")
        }
    }
}

旧的"正确"写法需要手动管理 timer,繁琐且容易出错:

timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
select {
case result := <-process(item):
    if !timer.Stop() {
        <-timer.C
    }
    handle(result)
case <-timer.C:
    log.Println("timeout")
}

Go 1.23 的修复

Go 1.23 修复了这个问题:time.Aftertime.NewTimertime.NewTicker 创建的计时器,在没有引用时可以被 GC 回收,即使还没有触发。

// Go 1.23 之后,这段代码不再有内存泄漏问题
func processWithTimeout(items []Item) {
    for _, item := range items {
        select {
        case result := <-process(item):
            handle(result)
        case <-time.After(5 * time.Second):
            log.Println("timeout")
            // timer 在下次 GC 时被回收
        }
    }
}

对于大量使用 time.After 的代码,升级到 1.23 后内存使用会有可观察的改善。详见 time.Timer 变更说明

链接器改进

Go 1.23 的链接器优化也值得一提:

  • 二进制体积缩减:更激进的死代码消除,典型 Go 程序二进制大小减少约 1-2%
  • 构建速度提升:链接阶段速度提升,大型项目更明显
  • PGO 扩展:Profile Guided Optimization 覆盖更多优化场景,升级版本即可受益,无需改代码

展望 Go 1.24(2025年2月发布)

2025 年 2 月发布的 Go 1.24 带来了几个重要改进:

  • 弱指针(Weak Pointers)weak 包引入弱引用,持有对象引用而不阻止 GC,适用于缓存场景
  • Map 性能改进:Swiss Map 实现(受 Abseil 启发),高负载下性能显著提升
  • 工具链管理增强go get 直接管理 Go 工具链版本,类似 npm 管理 Node 版本
  • 测试改进testing/synctest 包提供对并发测试的更好支持

Go 1.24 的 map 改进特别值得关注——对 map 密集型应用(路由表、状态机、高频查找)升级可以带来可观察的性能提升。

Go 语言的演进节奏稳定,不追求大跨步,每个版本都在实际开发体验上做切实的改进。range-over-func 讨论了多年,落地时设计得很干净——这是 Go 团队的一贯风格:宁可慢,不引入混乱。

参考:Go 1.23 Release Notes · Go 1.24 Release Notes · iter 包文档 · Go 官方博客