0

0

Golangfor range循环遍历数组切片map

P粉602998670

P粉602998670

发布时间:2025-09-22 20:58:01

|

361人浏览过

|

来源于php中文网

原创

for range是Go语言遍历集合的推荐方式,可简洁地访问数组、切片、map和字符串的索引(或键)与值;遍历时value为元素副本,修改它不影响原集合,但若元素是指针,则可通过副本指针修改其所指向的数据;在迭代中修改切片需用传统for循环避免越界或跳过问题,遍历map时禁止同时增删键值对,否则会panic;与goroutine结合时,因循环变量被重用,直接捕获会导致所有协程读取到相同值,正确做法是创建局部副本或通过函数参数传递当前值以确保每个goroutine使用独立拷贝。

golangfor range循环遍历数组切片map

for range
在 Go 语言里,就是你遍历数组、切片、map 乃至字符串的那个“瑞士军刀”。它提供了一种简洁又地道的方式来访问集合中的元素,每次迭代都会给你带来元素的索引(或键)和对应的值。可以说,掌握它,你就掌握了 Go 集合操作的核心脉络。

解决方案

在 Go 语言中,

for range
循环是处理各种集合类型最常见也最推荐的方式。它的语法简洁直观,能够优雅地遍历数组、切片和 map。

1. 遍历数组 (Arrays)

数组在 Go 中是定长的,

for range
遍历数组时,每次迭代会返回两个值:当前元素的索引和该元素的副本。

立即学习go语言免费学习笔记(深入)”;

package main

import "fmt"

func main() {
    numbers := [5]int{10, 20, 30, 40, 50}

    fmt.Println("遍历数组:")
    for index, value := range numbers {
        fmt.Printf("索引: %d, 值: %d\n", index, value)
    }

    // 如果只需要值,可以忽略索引
    fmt.Println("\n只遍历数组的值:")
    for _, value := range numbers {
        fmt.Printf("值: %d\n", value)
    }

    // 如果只需要索引,可以忽略值
    fmt.Println("\n只遍历数组的索引:")
    for index := range numbers {
        fmt.Printf("索引: %d\n", index)
    }
}

2. 遍历切片 (Slices)

切片是 Go 中更常用的动态数组,

for range
遍历切片的方式与数组几乎一致。同样,它也会返回索引和元素的副本。

package main

import "fmt"

func main() {
    fruits := []string{"Apple", "Banana", "Cherry"}

    fmt.Println("遍历切片:")
    for i, fruit := range fruits {
        fmt.Printf("索引: %d, 水果: %s\n", i, fruit)
    }

    // 注意:这里的 fruit 是元素的副本。修改 fruit 不会影响原始切片。
    fmt.Println("\n尝试修改切片元素副本:")
    for i, fruit := range fruits {
        fruit = "Modified " + fruit // 这不会改变原始切片
        fmt.Printf("循环内 (副本): %s\n", fruit)
        // 要修改原始切片,你需要使用索引
        // fruits[i] = "Modified " + fruits[i]
    }
    fmt.Println("循环后切片:", fruits) // 原始切片未变
}

3. 遍历 Map (Maps)

for range
遍历 map 时,每次迭代会返回键和对应的值。需要注意的是,map 的遍历顺序是不确定的,每次运行程序,你可能会看到不同的遍历顺序。

package main

import "fmt"

func main() {
    ages := map[string]int{
        "Alice": 30,
        "Bob":   24,
        "Charlie": 35,
    }

    fmt.Println("遍历 Map:")
    for name, age := range ages {
        fmt.Printf("姓名: %s, 年龄: %d\n", name, age)
    }

    // 如果只需要键
    fmt.Println("\n只遍历 Map 的键:")
    for name := range ages {
        fmt.Printf("姓名: %s\n", name)
    }

    // 如果只需要值
    fmt.Println("\n只遍历 Map 的值:")
    for _, age := range ages {
        fmt.Printf("年龄: %d\n", age)
    }
}

for range
迭代时,值是副本还是引用?深入理解 Go 语言的变量捕获机制

这是一个非常关键的问题,也是 Go 语言初学者常犯错误的地方。简单来说,当你使用

for index, value := range collection
这种形式时,
value
变量在每次迭代中得到的都是集合元素的副本。这意味着,如果你在循环体内修改
value
,它不会影响到原始集合中的元素。

我记得自己刚开始写 Go 的时候,就因为不理解这个机制,试图在循环里直接改

value
结果发现集合没变,愣了好一会儿。后来才明白,Go 这么设计是为了避免一些潜在的副作用,让代码行为更可预测。

举个例子,假设你有一个

[]struct
切片,你想修改切片里每个结构体的某个字段:

type Person struct {
    Name string
    Age  int
}

func main() {
    people := []Person{
        {"Alice", 30},
        {"Bob", 25},
    }

    fmt.Println("原始切片:", people)

    // 错误示范:修改副本,不影响原始切片
    for _, p := range people {
        p.Age += 1 // 这里修改的是 p 的副本,原始切片中的元素不会改变
    }
    fmt.Println("修改副本后:", people) // 仍然是 [Alice 30, Bob 25]

    // 正确做法:通过索引修改原始切片
    for i := range people {
        people[i].Age += 1 // 通过索引直接访问并修改原始元素
    }
    fmt.Println("通过索引修改后:", people) // 变成 [Alice 31, Bob 26]
}

但这里有个微妙之处。如果

value
本身是一个指针,或者一个包含指针的结构体,那么
value
的副本依然是那个指针的副本。这意味着,虽然指针本身被复制了,但它所指向的底层数据仍然是同一个。所以,通过这个复制的指针去修改它所指向的数据,是会影响到原始集合的。这在处理复杂数据结构时需要特别小心,很容易混淆。

比如,一个

[]*Person
切片:

func main() {
    peoplePtrs := []*Person{
        &Person{"Alice", 30},
        &Person{"Bob", 25},
    }

    fmt.Println("原始指针切片:", peoplePtrs[0].Age, peoplePtrs[1].Age)

    for _, p := range peoplePtrs {
        p.Age += 1 // p 是指针的副本,但它指向的还是原始 Person 对象
    }
    fmt.Println("修改指针指向的值后:", peoplePtrs[0].Age, peoplePtrs[1].Age) // 变成 31, 26
}

你看,这里的行为就完全不同了。所以,理解

value
是副本,但副本的“内容”是什么,是值类型还是引用类型,这决定了你在循环里能做什么。

Angular组件库
Angular组件库

本组件封装了Angular1.0版本,组件实现了以下功能: 路由,子路由,轮播,cookie读写,加密,表单提交验证,拦截器,白名单,搜索过滤与排序(等级划分), 大小写转换,Map数组循环遍历动态修改后台数据等功能。

下载

什么时候不该用
for range
?性能考量与特殊场景下的替代方案

for range
大部分时候都很好用,但在某些特定场景下,它可能不是最佳选择,甚至会带来意想不到的问题。我个人在项目中遇到过几次,就是因为没有考虑到这些特殊情况,导致代码行为异常。

一个主要的问题是在迭代过程中修改集合

  1. 修改切片(删除或添加元素): 如果你在

    for range
    循环中删除或添加切片元素,会变得非常棘手。
    for range
    在循环开始时会“记住”切片的长度和容量。如果你在循环体内修改切片的长度,可能会导致跳过元素、重复处理元素,甚至访问到越界索引。

    比如,你想从切片中移除所有偶数:

    func main() {
        nums := []int{1, 2, 3, 4, 5, 6}
        fmt.Println("原始切片:", nums)
    
        // 错误示范:在 for range 中修改切片长度
        for i, n := range nums {
            if n%2 == 0 {
                nums = append(nums[:i], nums[i+1:]...) // 移除元素
                // 这里的问题是,切片的长度和后续元素的索引都变了,但 i 还在正常递增
                // 导致可能跳过下一个元素,或者访问越界
            }
        }
        fmt.Println("尝试移除偶数后 (错误):", nums) // 结果可能不是你想要的
        // 例如,如果 2 被移除,3 会移动到索引 1,但 i 接着会变成 2,跳过了 3
    }

    正确的方法通常是使用传统的

    for
    循环,并小心地调整索引,或者创建一个新的切片来存储符合条件的元素。

    func main() {
        nums := []int{1, 2, 3, 4, 5, 6}
        fmt.Println("原始切片:", nums)
    
        // 正确做法:使用传统 for 循环并调整索引
        for i := 0; i < len(nums); { // 注意这里没有 i++
            if nums[i]%2 == 0 {
                nums = append(nums[:i], nums[i+1:]...)
            } else {
                i++ // 只有不移除元素时才递增索引
            }
        }
        fmt.Println("正确移除偶数后:", nums) // [1 3 5]
    }
  2. 修改 map(添加或删除键值对: 在

    for range
    遍历 map 的过程中修改 map 是未定义行为。Go 运行时会检测到这种操作,并通常会抛出
    panic
    。这是为了避免在迭代过程中因为 map 内部结构变化而导致的数据不一致或无限循环。

    func main() {
        m := map[string]int{"a": 1, "b": 2, "c": 3}
        fmt.Println("原始 map:", m)
    
        // 错误示范:在 for range 中修改 map
        for k, v := range m {
            if k == "b" {
                delete(m, "c") // 删除元素
                m["d"] = 4     // 添加元素
            }
            fmt.Printf("键: %s, 值: %d\n", k, v)
        }
        // 这段代码很可能会在运行时 panic: concurrent map iteration and map write
    }

    如果你需要在遍历 map 的同时修改它,通常的做法是:先遍历 map 收集需要修改的键,然后在遍历结束后再进行修改操作。

  3. 需要反向遍历

    for range
    总是从头到尾遍历。如果你需要从后往前遍历一个数组或切片,传统的
    for
    循环 (
    for i := len(slice)-1; i >= 0; i--
    ) 会是更直接、更清晰的选择。

总的来说,

for range
性能通常很好,但它的核心限制在于它为迭代提供了一个相对固定的“快照”视图。当你需要打破这个视图,动态地改变集合结构时,就需要考虑其他更精细的控制方式了。

for range
与并发:协程中捕获变量的陷阱与最佳实践

在 Go 语言中,

for range
循环与
goroutine
(协程)结合使用时,会遇到一个非常经典的“变量捕获陷阱”。这在我刚接触并发编程时,着实让我困惑了一阵子,因为结果总是出乎意料。

问题出在

for range
循环变量的生命周期和作用域上。
for range
循环中的
index
value
变量在每次迭代时都会被重用。这意味着,它们在内存中是同一个变量,只是在每次迭代时被赋予了新的值。

当你在循环内部启动一个

goroutine
,并且这个
goroutine
尝试去访问循环变量时,它捕获的实际上是循环变量的地址。由于
goroutine
是异步执行的,当它真正开始执行时,循环可能已经完成了,或者
index
value
变量已经被更新到了循环的最后一个值。结果就是,所有的
goroutine
都可能打印出或使用了相同的(通常是最后一个)值。

看一个例子,你可能一眼就能看出问题所在:

package main

import (
    "fmt"
    "time"
)

func main() {
    numbers := []int{1, 2, 3}

    fmt.Println("错误示范:goroutine 捕获循环变量")
    for i, n := range numbers {
        go func() {
            // 这里 i 和 n 都是被重用的循环变量
            // 当 goroutine 真正执行时,它们可能已经变成循环的最终值
            fmt.Printf("索引: %d, 值: %d\n", i, n)
        }()
    }
    time.Sleep(time.Millisecond * 100) // 等待 goroutine 执行
    fmt.Println("--------------------")

    fmt.Println("正确做法1:创建循环变量的局部副本")
    for i, n := range numbers {
        // 在循环内部为每个 goroutine 创建一个独立的变量副本
        // 这样每个 goroutine 都能捕获到当前迭代的正确值
        iCopy := i
        nCopy := n
        go func() {
            fmt.Printf("索引: %d, 值: %d\n", iCopy, nCopy)
        }()
    }
    time.Sleep(time.Millisecond * 100)
    fmt.Println("--------------------")

    fmt.Println("正确做法2:通过函数参数传递循环变量")
    for i, n := range numbers {
        // 将循环变量作为参数传递给匿名函数
        // 这样参数在函数调用时就会被复制,每个 goroutine 都会有自己的副本
        go func(index, value int) {
            fmt.Printf("索引: %d, 值: %d\n", index, value)
        }(i, n) // 在这里传递 i 和 n 的当前值
    }
    time.Sleep(time.Millisecond * 100)
}

运行第一个错误示范,你很可能会看到类似这样的输出:

索引: 2, 值: 3
索引: 2, 值: 3
索引: 2, 值: 3

而不是你期望的

(0, 1), (1, 2), (2, 3)
。这就是因为
i
n
在所有
goroutine
实际执行时,都已经更新到了循环的最后一个值。

解决这个问题的方法有两种,都是围绕着“为每个

goroutine
提供它自己专属的变量副本”这个核心思想:

  1. 创建局部副本:在
    for range
    循环内部,紧接着声明并初始化一个新的局部变量,将循环变量的值赋给它。这个新的局部变量在每次迭代中都会被重新创建,拥有独立的作用域,因此每个
    goroutine
    都能捕获到它自己的那份正确值。
  2. 通过函数参数传递:将循环变量作为参数传递给
    goroutine
    启动的匿名函数。当函数被调用时,参数会被复制,所以每个
    goroutine
    都会收到当前迭代的正确值。这种方法在我看来更简洁,也更符合函数参数传递的语义。

这个陷阱在 Go 语言的并发编程中非常普遍,几乎是每个 Go 开发者都会遇到的“成人礼”。理解并掌握这两种解决办法,能帮你避免很多潜在的并发 bug,让你的协程按预期工作。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

298

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

212

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1498

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

623

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

592

2024.03.22

php中定义字符串的方式
php中定义字符串的方式

php中定义字符串的方式:单引号;双引号;heredoc语法等等。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

587

2024.04.29

go语言字符串相关教程
go语言字符串相关教程

本专题整合了go语言字符串相关教程,阅读专题下面的文章了解更多详细内容。

170

2025.07.29

c++字符串相关教程
c++字符串相关教程

本专题整合了c++字符串相关教程,阅读专题下面的文章了解更多详细内容。

83

2025.08.07

Python 自然语言处理(NLP)基础与实战
Python 自然语言处理(NLP)基础与实战

本专题系统讲解 Python 在自然语言处理(NLP)领域的基础方法与实战应用,涵盖文本预处理(分词、去停用词)、词性标注、命名实体识别、关键词提取、情感分析,以及常用 NLP 库(NLTK、spaCy)的核心用法。通过真实文本案例,帮助学习者掌握 使用 Python 进行文本分析与语言数据处理的完整流程,适用于内容分析、舆情监测与智能文本应用场景。

9

2026.01.27

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

相关下载

更多

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
golang socket 编程
golang socket 编程

共2课时 | 0.1万人学习

nginx浅谈
nginx浅谈

共15课时 | 0.8万人学习

golang和swoole核心底层分析
golang和swoole核心底层分析

共3课时 | 0.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号