0

0

Go语言中Map遍历的指针陷阱:理解循环变量的地址行为与解决方案

DDD

DDD

发布时间:2025-11-30 22:15:01

|

959人浏览过

|

来源于php中文网

原创

Go语言中Map遍历的指针陷阱:理解循环变量的地址行为与解决方案

本文深入探讨了go语言中遍历map时,对循环变量直接取地址可能导致的常见陷阱。当在`for...range`循环中尝试获取`res`(值类型)的地址并存储时,由于`res`是循环变量的副本且其内存地址在迭代中被重用,最终会导致存储的多个指针都指向同一个内存位置,从而产生意料之外的重复地址问题。文章提供了两种有效的解决方案:一是将map的值类型改为指针类型,二是显式创建循环变量的副本再取地址,确保每个存储的指针都指向独立的内存对象。

Go语言中for...range循环变量的特性

在Go语言中,for...range循环是遍历切片、数组、字符串、Map和通道的常用方式。当遍历Map时,range会返回键和值的副本。这意味着在for key, value := range myMap这样的结构中,value是一个新的变量,它在每次迭代时都会被赋予Map中对应元素的副本。

理解这一点至关重要:value(或本例中的res)在循环的每次迭代中,其内存地址通常是固定的,只是它所存储的“内容”会随着Map中不同元素的赋值而更新。

问题重现:Map遍历中的指针陷阱

考虑以下场景,我们有一个存储Result结构体(值类型)的Map,并尝试在遍历时获取每个Result的地址并存储到一个切片中:

package main

import "fmt"

// Result 结构体用于示例
type Result struct {
    Port int
}

func main() {
    m := make(map[string]Result)
    m["server1"] = Result{Port: 6379}
    m["server2"] = Result{Port: 6380}

    // 初始化一个存储 *Result 指针的切片
    r := make([]*Result, len(m)) 
    i := 0
    for _, res := range m { // res 是 Result 类型的值副本
        // 打印当前迭代的 res 值和 res 变量的内存地址
        fmt.Printf("Iteration %d: res value = %+v, address of res variable = %p\n", i, res, &res)

        // 将循环变量 res 的地址存储到切片中
        r[i] = &res 
        i++
    }

    fmt.Println("\n--- 遍历结束后切片 r 的内容 ---")
    // 打印切片 r 中存储的指针及其指向的值
    for idx, ptr := range r {
        // 注意:*ptr 会显示循环结束后 res 的最终值
        fmt.Printf("r[%d] points to address %p, value = %+v\n", idx, ptr, *ptr)
    }
}

运行上述代码,你可能会得到类似如下的输出(具体地址值可能不同):

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

Iteration 0: res value = {Port:6379}, address of res variable = 0xc0000100a0
Iteration 1: res value = {Port:6380}, address of res variable = 0xc0000100a0

--- 遍历结束后切片 r 的内容 ---
r[0] points to address 0xc0000100a0, value = {Port:6380}
r[1] points to address 0xc0000100a0, value = {Port:6380}

从输出中可以清楚地看到问题:

  1. 在每次迭代中,res的值确实是Map中不同的Result结构体({Port:6379}和{Port:6380})。
  2. 然而,res变量本身的内存地址(address of res variable)在两次迭代中是相同的(例如0xc0000100a0)。
  3. 最终,切片r中存储的两个指针都指向了这个相同的内存地址。由于这个地址最终被{Port:6380}覆盖,所以r中的所有指针都指向了Map中最后一个元素的值。

根本原因分析

这个问题的核心在于for...range循环中res变量的生命周期和内存分配机制。

a0.dev
a0.dev

专为移动端应用开发设计的AI编程平台

下载
  • res是循环作用域内的一个局部变量。
  • 在每次迭代开始时,Go运行时会将Map中当前元素的值副本赋值给res。
  • res变量的内存空间在整个循环过程中是复用的,它的地址不会改变。
  • 当我们执行r[i] = &res时,我们存储的是这个循环变量res的地址,而不是Map中原始元素的地址,也不是每次迭代中res所持有的值的独立副本的地址。
  • 当循环结束后,res变量仍然存在,并持有Map中最后一个元素的值。因此,所有指向&res的指针都将指向这个最终的值。

解决方案一:将Map的值类型定义为指针

最直接且推荐的解决方案是,如果你的业务逻辑允许,将Map的值类型本身定义为指针类型。这样,Map存储的就已经是Result结构体的指针,for...range循环变量res也将是一个指针。直接存储res即可,因为它已经指向了独立的Result实例。

package main

import "fmt"

type Result struct {
    Port int
}

func main() {
    // Map存储 Result 结构体的指针
    m := make(map[string]*Result)
    m["server1"] = &Result{Port: 6379} // 存储指针
    m["server2"] = &Result{Port: 6380} // 存储指针

    r := make([]*Result, len(m))
    i := 0
    for _, res := range m { // res 此时已经是 *Result 类型(一个指针)
        // 打印当前迭代的 res 指针的值和 res 变量的内存地址,以及 res 指向的值
        fmt.Printf("Iteration %d: res value = %+v, address of res variable = %p, value pointed to by res = %p\n", i, *res, &res, res)

        r[i] = res // 直接存储 res (它本身就是一个指向 Result 结构体的指针)
        i++
    }

    fmt.Println("\n--- 遍历结束后切片 r 的内容 ---")
    for idx, ptr := range r {
        fmt.Printf("r[%d] points to address %p, value = %+v\n", idx, ptr, *ptr)
    }
}

输出示例:

Iteration 0: res value = {Port:6379}, address of res variable = 0xc0000100a0, value pointed to by res = 0xc0000120e0
Iteration 1: res value = {Port:6380}, address of res variable = 0xc0000100a0, value pointed to by res = 0xc0000120f0

--- 遍历结束后切片 r 的内容 ---
r[0] points to address 0xc0000120e0, value = {Port:6379}
r[1] points to address 0xc0000120f0, value = {Port:6380}

可以看到,此时r中存储的是不同的指针地址(0xc0000120e0和0xc0000120f0),它们分别指向了Map中原始的Result结构体实例,解决了重复地址的问题。

解决方案二:显式创建循环变量的副本

如果Map的值类型必须是值类型(例如出于内存、性能或语义上的考虑),那么我们可以在循环内部显式地创建一个res的副本,然后获取这个副本的地址。这样,每次迭代都会创建一个新的副本,并获得其独立的内存地址。

package main

import "fmt"

type Result struct {
    Port int
}

func main() {
    // Map存储 Result 结构体的值类型
    m := make(map[string]Result)
    m["server1"] = Result{Port: 6379}
    m["server2"] = Result{Port: 6380}

    r := make([]*Result, len(m))
    i := 0
    for _, res := range m { // res 仍然是 Result 类型的值副本
        fmt.Printf("Iteration %d: res value = %+v, address of res variable = %p\n", i, res, &res)

        // 显式创建 res 的副本,并获取副本的地址
        temp := res // 创建一个 res 的新副本
        r[i] = &temp // 存储副本的地址
        i++
    }

    fmt.Println("\n--- 遍历结束后切片 r 的内容 ---")
    for idx, ptr := range r {
        fmt.Printf("r[%d] points to address %p, value = %+v\n", idx, ptr, *ptr)
    }
}

输出示例:

Iteration 0: res value = {Port:6379}, address of res variable = 0xc0000100a0
Iteration 1: res value = {Port:6380}, address of res variable = 0xc0000100a0

--- 遍历结束后切片 r 的内容 ---
r[0] points to address 0xc0000120e0, value = {Port:6379}
r[1] points to address 0xc0000120f0, value = {Port:6380}

此方案同样有效。temp变量在每次迭代中都是一个新的局部变量,拥有独立的内存地址。因此,&temp会产生不同的指针,指向不同的Result副本。

选择合适的方案与注意事项

  • *Map存储指针类型 (`map[string]Result`)**:
    • 优点:代码简洁,直接存储res即可。Map中存储的是引用,修改Result实例会影响所有指向它的指针。对于大型结构体,可以减少内存复制开销。
    • 缺点:需要手动管理Result实例的创建(例如使用`&Result{

相关专题

更多
string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

338

2023.08.02

js 字符串转数组
js 字符串转数组

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

258

2023.08.03

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

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

209

2023.09.04

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

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

1468

2023.10.24

字符串介绍
字符串介绍

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

620

2023.11.24

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

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

550

2024.03.22

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

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

566

2024.04.29

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

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

166

2025.07.29

Java编译相关教程合集
Java编译相关教程合集

本专题整合了Java编译相关教程,阅读专题下面的文章了解更多详细内容。

9

2026.01.21

热门下载

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

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Go 教程
Go 教程

共32课时 | 4万人学习

Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

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

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