bigywater.tech
Go · 30

range-over-func 迭代器:yield 是怎么被编译成回调的

range-over-func 让 for range 可以直接遍历一个函数。它不是魔法:循环体被编译成一个回调,break、return 等被改写成状态变量。这篇用真实输出验证每种控制流的语义,看调用栈与符号,看违规时的 panic,最后量出它的成本。

main-range3
循环体被编译成的函数名:调用栈里 main.main-range3 ← main.Count.func1 ← main.main
6.7 倍
迭代器没有被内联时,对 1000 个 int 求和比切片循环慢(2033 ns vs 302 ns)
304 ns
迭代器与循环体都能内联时,与切片循环一样快(0 次分配)

控制流怎么映射

实测 · 本机执行输出
  for v := range Count(3) 手写等价形式:Count(3)(func(v int) bool { ... return true })
    循环体 v = 0
    循环体 v = 1
    循环体 v = 2
    [迭代器] 退出
  break 与 continue:
    循环体 v = 0
    循环体 v = 2
    [迭代器] yield(3) 返回了 false,停止
    [迭代器] 退出
  return 在循环体里:find(5, 2) 返回:
    [迭代器] yield(2) 返回了 false,停止
    [迭代器] 退出
    20 true
  defer 写在循环体里:要等到外层函数结束才执行
    [迭代器] 退出
    循环结束,函数还没返回
    [defer] 来自循环体 v = 1
    [defer] 来自循环体 v = 0

循环体就是传给迭代器的 yield 函数:continue 是返回 true,break 是返回 false(迭代器应当随即清理并返回),return 与外层 defer 由编译器用状态变量保证它们属于外层函数——实验里 return 之后迭代器先退出、外层函数再返回,循环体里的 defer 一直等到外层函数结束。

调用栈

实测 · 本机执行输出
   main.main-range3 ← main.Count.func1 ← main.main ← runtime.main ← runtime.goexit
    [迭代器] 退出

在循环体里取调用栈能直接看到这个实现:main.main-range3 是循环体,它的调用者是迭代器 main.Count.func1。panic 或调试时看到 -rangeN 就知道是循环体。

违规的迭代器

实测 · 本机执行输出
  break 之后迭代器继续 yield → panic:runtime error: range function continued iteration after function for loop body returned false
  循环结束后保存的 yield 再被调用 → panic:runtime error: range function continued iteration after whole loop exit

迭代器必须在 yield 返回 false 之后马上返回,也不能在循环结束后再调用 yield。运行时会检查这两种情况并 panic,消息里直接写明原因。这类检查保证了 break 等控制流的改写不会被迭代器破坏。

标准库

实测 · 本机执行输出
  slices.Sorted(slices.Values(xs)) = [a b c];slices.Collect(maps.Keys(m)) 排序后 = [x y]
  slices.Collect(strings.SplitSeq("a,b,c", ",")) = ["a" "b" "c"]
  strings.Lines 逐行 = ["one" "two" "three"]
  slices.All → (0, "p")
  slices.All → (1, "q")

slices、maps 里有成对的迭代器与收集函数(Values、All、Keys、Collect、Sorted),strings 里有 SplitSeq、Lines 等不先分配切片的版本。上面用真实输出演示了它们。

编译结果

实测 · 本机执行输出
  main.go:12:6: can inline Count
  main.go:14:3: can inline Count.func1.deferwrap1
  main.go:25:2: can inline find-range1
  main.go:59:4: can inline main.func1-range1.deferwrap1
  <GOROOT>/src/slices/iter.go:51:2: can inline slices.AppendSeq[go.shape.[]go.shape.string,go.shape.string]-range1
  main.go:46:2: can inline main-range1
  main.go:48:2: can inline main-range2
  main.go:65:2: can inline main-range3
  main.go:77:52: can inline main.func4-range1
  main.go:80:62: can inline main.func6-range1
  go tool nm 里 main 包的相关函数符号:
    main.find
    main.main
    main.Count.func1
    main.Count.func1.deferwrap1
    main.find-range1
    main.main.func6
    main.main.func6-range1
    main.main.func5
    main.main.func4
    main.main-range3
    main.main-range2
    main.main-range1
    main.main.func1
    main.Count.1
    main.Count.1.deferwrap1
    main.main.func1-range1
    main.main.func1-range1.deferwrap1
    main.main.func2
    main.main.func3
    main.main.func3.1

-m 与符号表里能看到 -range1、-range2 这类函数,循环体里的 defer 被包装成 deferwrap。这些都是实测输出;编译器重写控制流的具体算法(状态变量的布局等)我没有去读源码。

成本

实测 · 本机执行输出
  for range 切片                         中位 302.04 ns/op(最小 300.33 / 最大 326.47)
  for range Values(xs)(自写迭代器)          中位 2033.25 ns/op(最小 2027.59 / 最大 2078.67)
  for range slices.Values(xs)          中位 2014.60 ns/op(最小 2007.39 / 最大 2023.19)
  迭代器与循环体都不加 //go:noinline             中位 304.06 ns/op(最小 301.62 / 最大 316.71)
  回调函数 SumCallback(xs, f)              中位 1923.16 ns/op(最小 1915.70 / 最大 1925.83)
  AllocsPerRun:SumSeq(Values(xs)) 3 次;SumInl(xs)(不加 noinline)0 次;SumSlice 0 次

没有内联时每个元素是一次间接函数调用,约 2 ns,所以求和比切片循环慢约 6.7 倍(回调函数写法也一样);迭代器和循环体都能内联时,编译器把它还原成普通循环,耗时与切片相同、0 次分配。因此“range-over-func 一定慢”或“零成本”都不准确,要看是否内联。注意 //go:noinline 是为了对照而加的,正常代码里小的迭代器通常可以内联。

分步演示

range-over-func 迭代器
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。循环体的调用栈来自 runtime.Callers;-m 与符号表来自 go build -gcflags=-m 和 go tool nm;基准测试 7 轮取中位数,迭代器所在函数 //go:noinline(另有一个不加 noinline 的对照)
    • 没有验证:迭代器里 panic 的传播路径与 recover 的位置(见 Pull 一篇)、编译器改写 range-over-func 的具体算法、goto 与标签跳出循环体的各种情形、amd64 上的耗时、迭代器链(多层 Filter/Map)的开销
    京ICP备2026067502号-1京公网安备11010502063487号