Go 语言中 for 和 range 的性能比较

小林子
小林子
发布时间:2026-08-25| 39人浏览过
原创

前言

在 Go 项目里,`for` 和 `range` 看起来只是两种遍历写法,但一旦遇到大结构体、指针切片或需要原地修改元素,性能和语义都会出现明显分叉。本文按基准测试结果、底层拷贝行为与实战写法三条线展开,帮你判断什么时候优先选简洁,什么时候必须回到索引或指针。

在 Go 项目里,for 和 range 几乎每天都在用,真正容易踩坑的不是“会不会写”,而是它们在不同数据类型上的成本并不一样。下面就按常见使用场景拆开看:先对比写法和基准结果,再解释为什么大结构体会把差距放大,最后给出更适合日常开发的选型原则。

先看写法差异:什么时候更适合 for,什么时候更适合 range

传统 for 循环更强调控制力,适合需要索引、步进控制或直接修改原始元素的情况:

// 经典三段式
for i := 0; i < len(slice); i++ {
    // 通过索引访问元素
    element := slice[i]
}

它的优势是迭代过程完全可控,也更适合涉及原地修改的代码。

range 的写法则更紧凑,适合做普通遍历:

// 遍历值
for _, value := range slice {
    // 使用value
}

// 遍历索引和值
for index, value := range slice {
    // 使用index和value
}

它的优势是可读性高,少写边界处理,也天然适合数组、切片、map 和 channel 这类集合类型。

如果只从语法层面看,很多人会直接把 range 当作“更现代”的默认选项;但性能上能不能放心用,关键还是要看遍历的到底是什么数据。

三组基准测试:差距不是固定的,要看元素本身

基本类型切片:几乎可以按可读性来选

先看 []int 这种最常见的基本类型切片:

说明 range 值副本机制以及修改失效、索引回写和指针访问三种结果的关系图
range 为什么会改不到原值很多性能和修改问题,本质上都源自同一件事:range 拿到的是值变量,而不是默认拿到原元素引用。
用卡片对比 Go 中 for 与 range 在基本类型、大结构体和指针切片上的性能差异
三类遍历场景的性能分界线同样是遍历,真正决定差距的是元素拷贝成本,而不是语法表面是否简洁。
// 基准测试代码
func BenchmarkForIntSlice(b *testing.B) {
    nums := make([]int, 1<<20) // 100万个整数
    for i := 0; i < b.N; i++ {
        tmp := 0
        for j := 0; j < len(nums); j++ {
            tmp = nums[j]
        }
        _ = tmp
    }
}

func BenchmarkRangeIntSlice(b *testing.B) {
    nums := make([]int, 1<<20)
    for i := 0; i < b.N; i++ {
        tmp := 0
        for _, v := range nums {
            tmp = v
        }
        _ = tmp
    }
}

测试结果:

  • for:约 324,512 ns/op
  • range:约 322,744 ns/op

这个场景下,两者几乎没有实质差异。也就是说,如果你遍历的是 int、string、小型对象这类拷贝成本很低的数据,优先选择更简洁的 range 通常没有问题。

大结构体切片:性能差距会被迅速放大

真正的分水岭出现在大对象上:

type LargeItem struct {
    id  int
    val [4096]byte // 每个结构体占用4KB内存
}

func BenchmarkForStruct(b *testing.B) {
    var items [1024]LargeItem
    for i := 0; i < b.N; i++ {
        tmp := 0
        for j := 0; j < len(items); j++ {
            tmp = items[j].id
        }
        _ = tmp
    }
}

func BenchmarkRangeStruct(b *testing.B) {
    var items [1024]LargeItem
    for i := 0; i < b.N; i++ {
        tmp := 0
        for _, item := range items {
            tmp = item.id
        }
        _ = tmp
    }
}

测试结果:

  • for:约 324 ns/op
  • range:约 467,411 ns/op

这里的差距已经不是“略慢一点”,而是数量级上的区别。原因不在循环语法本身,而在于 range 取出的 item 是当前元素的一个副本;当每个元素都有 4KB 体积时,这个拷贝成本会在每次迭代里持续累积。

指针切片:range 又会重新变得划算

如果把大结构体改成指针切片,结果会明显不同:

func BenchmarkRangePointer(b *testing.B) {
    items := make([]*LargeItem, 1024)
    // 初始化指针切片...
    for i := 0; i < b.N; i++ {
        tmp := 0
        for _, item := range items {
            tmp = item.id
        }
        _ = tmp
    }
}

测试结果:

  • for:约 4,160 ns/op
  • range:约 4,194 ns/op

这时性能又重新接近了。因为遍历时被拷贝的不再是整个结构体,而只是一个指针,通常只需要 8 字节,成本自然低很多。

性能差异的根源:range 取到的是值副本

理解这一点,比记住“某种写法更快”更重要。很多 Go 代码的性能差异,其实都能回到同一个事实:range 在遍历时会把元素值取出来,赋给循环变量。

这个行为不仅影响性能,也会影响修改结果:

type Person struct{ Age int }
people := []Person{{30}, {40}}

// range修改无效(操作的是拷贝)
for _, p := range people {
    p.Age += 10 
}

// for修改有效
for i := range people {
    people[i].Age += 10 
}

为什么前一段修改无效?因为 p 只是切片元素的副本,改的是临时值,不会写回原切片。

把这个规则套回前面的测试,结论就很清楚了:

  • 基本类型:拷贝很便宜,for 和 range 差异通常可忽略
  • 大结构体:每次迭代都可能发生完整拷贝,成本会迅速上升
  • 指针类型:只拷贝指针本身,代价远低于拷贝整个对象

所以,性能敏感场景里真正要关注的不是“要不要统一用 range”,而是“当前循环变量拿到的到底是大对象本身,还是一个廉价引用”。

map 和 channel 的遍历要单独看

map:应直接使用 range

map 的遍历方式和切片不同,实践中应该直接用 range:

m := map[string]int{"A": 1, "B": 2}
for k, v := range m {
    // 迭代顺序不确定
    delete(m, "C") // 安全删除未迭代键
    m["D"] = 3     // 新增键可能被迭代到
}

这里需要注意两点:一是 map 的迭代顺序本来就不确定;二是在遍历过程中删除未迭代到的键是安全的,而新增键则“可能”被遍历到,不能依赖顺序和覆盖范围写逻辑。

对于 map,range 本身就是惯用且安全的写法,通常没有必要为了形式统一而绕成别的方案。

channel:range 是消费数据的标准写法

channel 的场景也类似:

ch := make(chan int, 10)
// ...发送数据...
for v := range ch {
    // 处理值,直到ch关闭
}

这类循环会持续接收值,直到 ch 被关闭。它是 Go 中消费通道数据最自然的方式。

需要额外记住的一点是:对 nil 信道使用 range 会永久阻塞。这不是性能问题,而是很典型的运行时行为问题,排查并发代码时经常会遇到。

实战怎么选:先看数据大小,再看是否修改元素

把前面的结论收束到日常开发里,可以按下面的思路选:

1. 基本类型切片:优先考虑代码简洁度

如果遍历的是基本类型切片,或者体积很小的对象,range 往往就是更省心的选择。性能差异很小,代码可读性通常更值得优先考虑。

2. 大内存结构体:避免直接用值遍历

当元素本身很大时,尽量不要写成直接取值的 range。更稳妥的方式有三种:

  • 使用传统 for 循环按索引访问
  • 改成指针切片 []*T 后再配合 range
  • 保留 range,但只遍历索引:for i := range items { items[i]... }

3. 需要修改原元素:不要直接改 range 的值变量

只要目标是修改集合中的原始元素,就不能把 range 取出的值变量当成原对象本身。应当使用索引回写,或者直接处理指针。

一个兼顾可读性和性能的常见写法是:

for i := range items {
    item := &items[i] // 获取指针避免拷贝
    item.Process()    // 直接修改原元素
}

4. map 和 channel:遵循语言惯用法

对于 map 和 channel,没有必要强行套用切片那一套比较方法。该用 range 时就直接用,重点是理解它们各自的行为边界。

最后还要补充一点:Go 1.20+ 对 range 在切片和 map 上的遍历性能做过优化。如果你的代码只涉及只读访问、基本类型或小对象,那么大多数情况下没必要为微小差异牺牲可读性。

归纳成一句话就是:拷贝成本高的地方优先考虑索引或指针,普通业务遍历优先考虑 range 的简洁性。 真正有价值的不是机械地站队,而是知道什么时候语法糖背后会带来真实代价。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。