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

  发布时间:2025-11-04 10:49:44   作者:玩站小弟   我要评论
我们知道,Go 的语法比较简洁。它并不提供类似 C 支持的 while、do...while 等循环控制语法,而仅保留了一种语句,即 for 循环。for i := 0; i & 。

我们知道,处理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]

但是,实际输出是b2b信息网

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 的原始值,而不是云南idc服务商修改后的值。

那么,问题来了,既然 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 循环差。

遍历结构体数组

上述性能测试中,源码库我们的遍历对象类型是 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

  • Tag:

相关文章

  • 神舟战神电脑装系统教程(一步步教你如何为神舟战神电脑装系统)

    摘要:对于购买了神舟战神电脑的用户来说,装系统是必不可少的一项任务。本文将以详细的步骤和说明,教你如何为神舟战神电脑装系统,让你能够轻松上手。1.确定装系统所需材料在进行系统...
    2025-11-04
  • TypeScript 终极初学者指南

    下面是我整理的一些初学者必备的一些知识点~Typescript 简介据官方描述:TypeScript 是 JavaScript 的超集,这意味着它可以完成 JavaScript 所做的所有事情,而且额
    2025-11-04
  • 我也是醉了,Eureka 延迟注册还有这个坑!

    Eureka 有个延迟注册的功能,也就是在服务启动成功之后不立刻注册到 Eureka Server,而是延迟一段时间再去注册,这样做的主要目的是因为虽然服务启动成功了,可能还有一些框架或者业务的代码没
    2025-11-04
  • 分布式、服务化的ERP系统架构设计

    ERP之痛 曾几何时,我混迹于电商、珠宝行业4年多,为这两个行业开发过两套大型业务系统ERP)。作为一个ERP系统,系统主要功能模块无非是订单管理、商品管理、生产采购、仓库管理、
    2025-11-04
  • 电脑安装无损分区教程(一步步教你如何在电脑上进行无损分区,保护你的数据安全)

    摘要:在电脑使用过程中,我们经常需要对硬盘进行分区,以便更好地管理和存储数据。然而,传统的分区方式往往会导致数据丢失的风险,因此无损分区成为了越来越多人的选择。本文将详细介绍如何利用电脑...
    2025-11-04
  • 更换域名有什么该注意的?新手要更换域名怎么办?

    更换域名有什么该注意的?新手要更换域名怎么办?不可避免的是,有时我们需要更改域名,要么是因为当前的域名不符合企业当前的发展,要么是因为有更好、更有价值的域名需要注册。更改域名或更改网址将给网站造成损失
    2025-11-04

最新评论