在 Go 语言中,range 是一个非常常用的结构,用于遍历集合类型的数据。它简洁、安全且易于使用,是 Go 开发者日常开发中最常使用的语法之一。
本文将深入讲解 Go 的 range 表达式的使用方式、返回值含义以及常见错误,并通过多个示例帮助你更好地理解和应用 range。
一、什么是 range?
range 是 Go 中用于迭代(遍历)集合类型的内置关键字,支持以下几种数据结构:
- 数组(Array)
- 切片(Slice)
- 字符串(String)
- Map(映射)
- 通道(Channel)
在遍历过程中,range 返回 两个值(对于 Channel 只返回一个值),具体取决于所遍历的对象类型。
二、range 的基本语法
for index, element := range collection {
// 使用 index 和 element
}
也可以只使用其中一个值,用 _ 忽略另一个:
for _, element := range collection { ... }
for index, _ := range collection { ... }
或者直接忽略索引或键:
for element := range collection { ... } // 只获取元素(不推荐,容易引起误解)
⚠️ 注意:Go 中如果定义了变量但没有使用,会编译报错。所以如果你不需要某个返回值,建议使用
_显式忽略。
三、不同数据类型下 range 返回的值
1. 遍历数组 / 切片
返回值: 索引(int)、元素(element)
nums := []int{10, 20, 30}
for i, num := range nums {
fmt.Printf("索引: %d, 值: %d\n", i, num)
}
输出:
索引: 0, 值: 10
索引: 1, 值: 20
索引: 2, 值: 30
仅需要元素时:
for _, num := range nums {
fmt.Println(num)
}
错误写法:
for num, _ := range nums {
fmt.Println(num) // 这里 num 实际上是索引!
}
❗ 错误原因:
range第一个返回值是索引,第二个才是元素。上面代码把索引赋给了num,而忽略了真正的元素。
2. 遍历字符串
返回值: 字符的位置索引(int)、字符的 Unicode 码点(rune)
s := "你好,世界"
for i, c := range s {
fmt.Printf("位置: %d, 字符: %c\n", i, c)
}
输出:
位置: 0, 字符: 你
位置: 3, 字符: 好
位置: 6, 字符: ,
位置: 9, 字符: 世
位置: 12, 字符: 界
注意:Go 中字符串是 UTF-8 编码的字节序列,使用
range可以正确处理中文等多字节字符。
3. 遍历 Map
返回值: 键(key)、值(value)
userMap := map[string]int{
"Alice": 25,
"Bob": 30,
"Charlie": 28,
}
for name, age := range userMap {
fmt.Printf("姓名: %s, 年龄: %d\n", name, age)
}
输出(顺序可能不同):
姓名: Alice, 年龄: 25
姓名: Bob, 年龄: 30
姓名: Charlie, 年龄: 28
只获取键:
for name := range userMap {
fmt.Println(name)
}
只获取值:
for _, age := range userMap {
fmt.Println(age)
}
4. 遍历 Channel(单值)
返回值: 接收到的值(channel 中的数据)
ch := make(chan int)
go func() {
ch <- 1
ch <- 2
close(ch)
}()
for v := range ch {
fmt.Println(v)
}
输出:
1
2
⚠️ 注意:只有在 channel 被关闭后,
range才会退出循环。
四、最佳实践和注意事项
1. 避免误用变量顺序
Go 中 range 返回两个值:第一个是索引或键,第二个是元素或值。如果你写反了顺序,Go 编译器不会报错,但会导致逻辑错误。
users := []User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
}
for user, _ := range users {
fmt.Println(user.ID) // 错误:user 实际上是索引 int 类型!
}
输出会出错,因为
user是int类型(索引),而不是User结构体。
正确写法:
for _, user := range users {
fmt.Println(user.ID)
}
2. 显式忽略不需要的值(推荐使用 _)
当你只关心元素或键时,应使用 _ 明确表示忽略另一个值,这样可以让代码意图更清晰,并避免编译器报错(Go 要求所有声明的变量都必须被使用)。
// 只需要元素
for _, user := range users {
fmt.Println(user.Name)
}
// 只需要索引
for idx, _ := range users {
fmt.Println(idx)
}
注意:不要为了省略一个
_而省略第一个参数,比如写成:for user := range users { ... }这种写法虽然合法,但容易引起误解,尤其是对新手来说,会以为
user是元素,而实际上它是索引(如果是切片/数组)或 key(如果是 map)。
3. 避免在循环中修改原切片内容(除非使用指针)
由于 range 是按值复制的方式遍历元素,因此在循环中直接修改元素字段是不会影响原始切片的。
type User struct {
ID int
Name string
}
users := []User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
}
for _, user := range users {
user.Name = "Updated" // ❌ 不会修改原始切片中的数据
}
正确做法是使用索引显式访问元素:
for i := range users {
users[i].Name = "Updated"
}
或者使用指针切片:
users := []*User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
}
for _, user := range users {
user.Name = "Updated" // 成功修改原始对象
}
4. 使用具名变量提升可读性
虽然你可以写成:
for i, u := range users {
fmt.Println(i, u.ID)
}
但这并不利于阅读。推荐使用更具描述性的变量名:
for index, user := range users {
fmt.Printf("第 %d 个用户:%s\n", index, user.Name)
}
这样可以让其他开发者更容易理解你的意图。
5. 在大集合中注意性能问题
range 是一种安全且方便的迭代方式,但在处理非常大的切片或 map 时,要注意以下几点:
- 每次迭代都会进行一次赋值操作(结构体拷贝),如果结构体较大,可能带来性能开销。
- 如果只是想查找特定条件的元素,考虑使用
for i := 0; i < len(...); i++或者结合break提前退出。 - 对于 map 来说,遍历顺序是随机的(每次运行结果可能不同),不能依赖顺序做判断。
6. 在嵌套结构中使用多重 range 要小心
例如,遍历二维数组或嵌套 map 时,要特别注意变量命名和作用域,避免混淆:
matrix := [][]int{
{1, 2},
{3, 4},
}
for i, row := range matrix {
for j, val := range row {
fmt.Printf("matrix[%d][%d] = %d\n", i, j, val)
}
}
建议在嵌套循环中使用不同的变量名(如
i, j),以增强可读性和避免冲突。
7. 在循环中使用 goroutine 的陷阱
如果你在 range 循环中启动 goroutine 并访问循环变量,需要注意变量捕获的问题:
users := []string{"Alice", "Bob", "Charlie"}
for _, name := range users {
go func() {
fmt.Println(name)
}()
}
上面的代码可能会输出多个
"Charlie",因为name是同一个变量地址,所有 goroutine 共享它。
正确做法是在循环体内创建副本:
for _, name := range users {
name := name // 创建副本
go func() {
fmt.Println(name)
}()
}
或者传参给函数:
for _, name := range users {
go func(n string) {
fmt.Println(n)
}(name)
}
8. 避免在 range 中频繁分配内存(优化建议)
如果你在 range 中做了大量重复的对象创建或字符串拼接操作,可以考虑复用对象或使用缓冲区(如 bytes.Buffer)来减少 GC 压力。
例如:
var sb strings.Builder
for _, user := range users {
sb.WriteString(fmt.Sprintf("%s, ", user.Name))
}
fmt.Println(sb.String())
这比每次都新建字符串效率更高。
使用 range 的黄金法则
| 原则 | 说明 |
|---|---|
| ✅ 明确变量用途 | 使用 index, element 等有意义的变量名 |
| ✅ 忽略不使用的值 | 用 _ 表示明确不想用某个返回值 |
| ✅ 避免误用顺序 | 不要把索引当作元素来使用 |
| ✅ 修改结构体需谨慎 | 若非指针切片,修改不会生效 |
| ✅ 处理并发时注意变量捕获 | 在 goroutine 中尽量使用局部副本 |
| ✅ 性能敏感场景考虑替代方案 | 如大数据量下使用普通 for 循环 |
通过遵循这些最佳实践,你可以写出更健壮、高效、易维护的 Go 代码,同时避免常见的逻辑错误和性能瓶颈。
如你正在构建大型项目、并发系统或高性能服务,掌握 range 的高级用法和潜在陷阱尤为重要。
五、总结
| 类型 | 第一个返回值 | 第二个返回值 |
|---|---|---|
| 切片/数组 | 索引 (int) | 元素 (element) |
| 字符串 | 索引 (int) | rune |
| Map | Key | Value |
| Channel | 接收的值 | —— |
| 写法 | 是否推荐 | 说明 |
|---|---|---|
for i, user := range users | ✅ 推荐 | 获取索引和元素 |
for _, user := range users | ✅ 推荐 | 忽略索引,只要元素 |
for user, _ := range users | ❌ 不推荐 | 错误地将索引赋给 user |
for user := range users | ⚠️ 警告 | 只获取索引或 key,容易误解 |
如果这篇文章对大家有帮助可以点赞关注,你的支持就是我的动力😊!



2025

被折叠的 条评论
为什么被折叠?



