range-over-func 让 for range 可以直接遍历一个函数。它不是魔法:循环体被编译成一个回调,break、return 等被改写成状态变量。这篇用真实输出验证每种控制流的语义,看调用栈与符号,看违规时的 panic,最后量出它的成本。
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 是为了对照而加的,正常代码里小的迭代器通常可以内联。