Go 中的 range 表达式详解:遍历数组、切片、字符串与 Map
在 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 <- 1ch <- 2close(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 intName 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,容易误解 |