当前位置: 当前位置:首页 >应用开发 >Go 处理大数组:使用 for range 还是 for 循环? 正文

Go 处理大数组:使用 for range 还是 for 循环?

2025-11-04 14:25:42 来源:多维IT资讯作者:域名 点击:610次

我们知道,处理Go 的大数语法比较简洁。它并不提供类似 C 支持的组使 while、do...while 等循环控制语法,还环而仅保留了一种语句,处理即 for 循环。大数

for i := 0; i < n; i++ {

Go 处理大数组:使用 for range 还是 for 循环?

... ...

}

但是组使,经典的还环三段式循环语句,需要获取迭代对象的处理长度 n。鉴于此,大数为了更方便 Go 开发者对复合数据类型进行迭代,组使例如 array、还环slice、处理channel、大数map,组使Go 提供了 for 循环的变体,即 for range 循环。

副本复制问题

range 在带来便利的同时,也给 Go 初学者带来了一些麻烦。因为使用者需要明白一点:for range 中,参与循环表达式的只是对象的副本。

func main() {

var a = [5]int{1, 2, 3, 4, 5}

var r [5]int

fmt.Println("original a =", a)

for i, v := range a {

if i == 0 {

a[1] = 12

a[2] = 13

}

r[i] = v

}

fmt.Println("after for range loop, r =", r)

fmt.Println("after for range loop, a =", a)

}

你认为这段代码会输出以下结果吗?

original a = [1 2 3 4 5]

after for range loop, r = [1 12 13 4 5]

after for range loop, a = [1 12 13 4 5]

但是,实际输出是香港云服务器

original a = [1 2 3 4 5]

after for range loop, r = [1 2 3 4 5]

after for range loop, a = [1 12 13 4 5]

为什么会这样?原因是参与 for range 循环是 range 表达式的副本。也就是说,在上面的例子中,实际上参与循环的是 a 的副本,而不是真正的 a。

为了让大家更容易理解,我们把上面例子中的 for range 循环改写成等效的伪代码形式。

for i, v := range ac { //ac is a value copy of a

if i == 0 {

a[1] = 12

a[2] = 13

}

r[i] = v

}

ac 是 Go 临时分配的连续字节序列,与 a 根本不是同一块内存空间。因此,无论 a 如何修改,它参与循环的副本 ac 仍然保持原始值,因此从 ac 中取出的 v 也依然是 a 的原始值,而不是IT技术网修改后的值。

那么,问题来了,既然 for range 使用的是副本数据,那 for range 会比经典的 for 循环消耗更多的资源并且性能更差吗?

性能对比

基于副本复制问题,我们先使用基准示例来验证一下:对于大型数组,for range 是否一定比经典的 for 循环运行得慢?

package main

import "testing"

func BenchmarkClassicForLoopIntArray(b *testing.B) {

b.ReportAllocs()

var arr [100000]int

for i := 0; i < b.N; i++ {

for j := 0; j < len(arr); j++ {

arr[j] = j

}

}

}

func BenchmarkForRangeIntArray(b *testing.B) {

b.ReportAllocs()

var arr [100000]int

for i := 0; i < b.N; i++ {

for j, v := range arr {

arr[j] = j

_ = v

}

}

}

在这个例子中,我们使用 for 循环和 for range 分别遍历一个包含 10 万个 int 类型元素的数组。让我们看看基准测试的结果。

$ go test -bench . forRange1_test.go

goos: darwin

goarch: amd64

cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz

BenchmarkClassicForLoopIntArray-8 47404 25486 ns/op 0 B/op 0 allocs/op

BenchmarkForRangeIntArray-8 37142 31691 ns/op 0 B/op 0 allocs/op

PASS

ok command-line-arguments 2.978s

从输出结果可以看出,for range 的确会稍劣于 for 循环,当然这其中包含了编译器级别优化的结果(通常是静态单赋值,或者 SSA 链接)。

让我们关闭优化开关,再次运行压力测试。

$ go test -c -gcflags -N -l . -o forRange1.test

$ ./forRange1.test -test.bench .

goos: darwin

goarch: amd64

pkg: workspace/example/forRange

cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz

BenchmarkClassicForLoopIntArray-8 6734 175319 ns/op 0 B/op 0 allocs/op

BenchmarkForRangeIntArray-8 5178 242977 ns/op 0 B/op 0 allocs/op

PASS

当没有编译器优化时,两种循环的性能都明显下降, for range 下降得更为明显,性能也更加比经典 for 循环差。

遍历结构体数组

上述性能测试中,b2b信息网我们的遍历对象类型是 int 值的数组,如果我们将 int 元素改为结构体会怎么样?for 和 for range 循环各自表现又会如何?

package main

import "testing"

type U5 struct {

a, b, c, d, e int

}

type U4 struct {

a, b, c, d int

}

type U3 struct {

b, c, d int

}

type U2 struct {

c, d int

}

type U1 struct {

d int

}

func BenchmarkClassicForLoopLargeStructArrayU5(b *testing.B) {

b.ReportAllocs()

var arr [100000]U5

for i := 0; i < b.N; i++ {

for j := 0; j < len(arr)-1; j++ {

arr[j].d = j

}

}

}

func BenchmarkClassicForLoopLargeStructArrayU4(b *testing.B) {

b.ReportAllocs()

var arr [100000]U4

for i := 0; i < b.N; i++ {

for j := 0; j < len(arr)-1; j++ {

arr[j].d = j

}

}

}

func BenchmarkClassicForLoopLargeStructArrayU3(b *testing.B) {

b.ReportAllocs()

var arr [100000]U3

for i := 0; i < b.N; i++ {

for j := 0; j < len(arr)-1; j++ {

arr[j].d = j

}

}

}

func BenchmarkClassicForLoopLargeStructArrayU2(b *testing.B) {

b.ReportAllocs()

var arr [100000]U2

for i := 0; i < b.N; i++ {

for j := 0; j < len(arr)-1; j++ {

arr[j].d = j

}

}

}

func BenchmarkClassicForLoopLargeStructArrayU1(b *testing.B) {

b.ReportAllocs()

var arr [100000]U1

for i := 0; i < b.N; i++ {

for j := 0; j < len(arr)-1; j++ {

arr[j].d = j

}

}

}

func BenchmarkForRangeLargeStructArrayU5(b *testing.B) {

b.ReportAllocs()

var arr [100000]U5

for i := 0; i < b.N; i++ {

for j, v := range arr {

arr[j].d = j

_ = v

}

}

}

func BenchmarkForRangeLargeStructArrayU4(b *testing.B) {

b.ReportAllocs()

var arr [100000]U4

for i := 0; i < b.N; i++ {

for j, v := range arr {

arr[j].d = j

_ = v

}

}

}

func BenchmarkForRangeLargeStructArrayU3(b *testing.B) {

b.ReportAllocs()

var arr [100000]U3

for i := 0; i < b.N; i++ {

for j, v := range arr {

arr[j].d = j

_ = v

}

}

}

func BenchmarkForRangeLargeStructArrayU2(b *testing.B) {

b.ReportAllocs()

var arr [100000]U2

for i := 0; i < b.N; i++ {

for j, v := range arr {

arr[j].d = j

_ = v

}

}

}

func BenchmarkForRangeLargeStructArrayU1(b *testing.B) {

b.ReportAllocs()

var arr [100000]U1

for i := 0; i < b.N; i++ {

for j, v := range arr {

arr[j].d = j

_ = v

}

}

}

在这个例子中,我们定义了 5 种类型的结构体:U1~U5,它们的区别在于包含的 int 类型字段的数量。

性能测试结果如下:

$ go test -bench . forRange2_test.go

goos: darwin

goarch: amd64

cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz

BenchmarkClassicForLoopLargeStructArrayU5-8 44540 26227 ns/op 0 B/op 0 allocs/op

BenchmarkClassicForLoopLargeStructArrayU4-8 45906 26312 ns/op 0 B/op 0 allocs/op

BenchmarkClassicForLoopLargeStructArrayU3-8 43315 27400 ns/op 0 B/op 0 allocs/op

BenchmarkClassicForLoopLargeStructArrayU2-8 44605 26313 ns/op 0 B/op 0 allocs/op

BenchmarkClassicForLoopLargeStructArrayU1-8 45752 26110 ns/op 0 B/op 0 allocs/op

BenchmarkForRangeLargeStructArrayU5-8 3072 388651 ns/op 0 B/op 0 allocs/op

BenchmarkForRangeLargeStructArrayU4-8 4605 261329 ns/op 0 B/op 0 allocs/op

BenchmarkForRangeLargeStructArrayU3-8 5857 182565 ns/op 0 B/op 0 allocs/op

BenchmarkForRangeLargeStructArrayU2-8 10000 108391 ns/op 0 B/op 0 allocs/op

BenchmarkForRangeLargeStructArrayU1-8 36333 32346 ns/op 0 B/op 0 allocs/op

PASS

ok command-line-arguments 16.160s

我们看到一个现象:不管是什么类型的结构体元素数组,经典的 for 循环遍历的性能比较一致,但是 for range 的遍历性能会随着结构字段数量的增加而降低。

带着疑惑,发现了一个与这个问题相关的 issue:cmd/compile: optimize large structs:https://img.ydisp.cn/news/20220902/ikhoigd4154 issue 大致是说:如果一个结构体类型有超过一定数量的字段(或一些其他条件),就会将该类型视为 unSSAable。如果 SSA 不可行,那么就无法通过 SSA 优化,这也是造成上述基准测试结果的重要原因。

结论

对于遍历大数组而言, for 循环能比 for range 循环更高效与稳定,这一点在数组元素为结构体类型更加明显。

另外,由于在 Go 中切片的底层都是通过数组来存储数据,尽管有 for range 的副本复制问题,但是切片副本指向的底层数组与原切片是一致的。这意味着,当我们将数组通过切片代替后,不管是通过 for range 或者 for 循环均能得到一致的稳定的遍历性能。

本文部分内容翻译整理自:Handling Large Arrays in Golang: Should You Use For Range or For Loop? https://betterprogramming.pub/handling-large-arrays-in-golang-should-you-use-for-range-or-for-loop-9995a02fd316

作者:人工智能
------分隔线----------------------------
头条新闻
图片新闻
新闻排行榜