Golang中如何通过指针修改map中的结构体字段

Golang中如何通过指针修改map中的结构体字段 你好。

我无法通过映射中的指针来设置结构体字段。最后两个打印语句应该输出44。

package main

import "fmt"

type T struct {
    n int
}

func main() {
    m := make(map[int]T)
    m[0] = T{n: 42}
    fmt.Println(m[0].n) // 输出 42

    // 尝试通过指针修改
    // m[0].n = 43 // 编译错误:无法分配给 m[0].n

    // 尝试获取地址
    // p := &m[0] // 编译错误:无法获取 m[0] 的地址

    // 尝试使用指针类型的映射
    mp := make(map[int]*T)
    mp[0] = &T{n: 42}
    fmt.Println(mp[0].n) // 输出 42
    mp[0].n = 44
    fmt.Println(mp[0].n) // 输出 44

    // 但如果值是通过索引表达式返回的呢?
    // 假设我们有一个返回 *T 的函数
    // 或者从另一个映射中获取
    m2 := make(map[int]T)
    m2[0] = T{n: 42}
    // 同样的问题:无法获取地址来修改
    // 所以我们需要存储指针,或者使用一个临时变量
    tmp := m2[0]
    tmp.n = 44
    m2[0] = tmp
    fmt.Println(m2[0].n) // 输出 44
}

The Go Play Space

感谢。


更多关于Golang中如何通过指针修改map中的结构体字段的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

谢谢!你知道我在哪里可以找到一些文档或类似资料来更详细地解释这一点吗?“切片条目的副本”对我来说仍然没有意义。切片条目难道不是指针吗?

更多关于Golang中如何通过指针修改map中的结构体字段的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


@jonasgheer

这确实是一个棘手的问题。

在 range 循环中:

	for _, r := range robots {
		robotMap[r.name] = &r
	}

r 是切片元素的副本

要获取切片元素本身,请使用循环索引:

	for i, r := range robots {
		robotMap[r.name] = &robots[i]
	}

The Go Play Space

The Go Play Space

具有语法高亮、海龟图形等功能的替代 Go (Golang) Playground

robots 被定义为 robots := []Robot{Robot{"Ronny", 33}},因此它的类型是 []Robot

当您在此处迭代 robots 时:

for _, r := range robots { /* ... */ }

r 是一个 Robot,而不是一个 *Robot。如果您想要元素的地址,那么您必须这样做:

for i := range robots {
    r := &robots[i]
    /* ... */
}

因此,当您获取 r 的地址时,它会强制 r 逃逸到堆上,并在内存中变成其自身独立的值。当您稍后修改它时,您修改的是副本。

我必须承认,这个问题难住我了!当我最初看到向切片添加另一个条目时发生的情况,我也感到惊讶:The Go Play Space

我也从未听说过 Go Play Space!

感谢发帖,以及出色的发现!

难道切片元素不是指针吗?

[]robots 的元素是 robot 类型,而 range 迭代器返回的正是这个类型。

语言规范没有直接提及复制,但在 For 语句 章节的“带 range 子句的 For 语句”小节中提到:

带有“range”子句的“for”语句会遍历数组、切片、字符串或映射的所有条目,或从通道接收的值。对于每个条目,如果存在迭代变量,它会将迭代值赋给相应的迭代变量,然后执行代码块。

根据定义,赋值是一种复制操作。因此,在 for _, r := range... 中,r 的内容是当前切片元素的副本。

这里有另一种解释。(厚颜自荐:这来自我的课程 Master Go。)


使用 range 操作符时有一个可能的陷阱。range 操作符在每次迭代中返回的元素值只是列表对象中元素的副本。因此,例如,如果我们尝试替换字符串中的一个字符,仅仅为元素变量分配一个新值是不够的。

s := []int{1, 2, 3}  // 一个整数切片,用三个值初始化
for _, element := range s {
    element = 4
    fmt.Println(element)
}
fmt.Println(s)

输出:

4
4
4
[1 2 3]

输出显示,在每次迭代中 element 被设置为 4,但循环之后,字节切片保持不变。

为了修改切片的内容,我们需要使用循环索引。

s := []int{1, 2, 3}
for index, _ := range s {
    s[index] = 4
}
fmt.Println(s)

当我们运行这个时,我们可以看到所有切片值都被修改了。

4
4
4
[4 4 4]

在Go语言中,无法直接获取map中结构体值的地址来修改字段,这是因为map内部实现机制导致的。当通过键访问map中的值时,返回的是值的副本,而不是原始值。因此,直接修改这个副本是无效的,编译器会阻止这种操作。

正确的做法是使用指针类型的map,或者通过临时变量进行修改后重新赋值。以下是针对你问题的具体解决方案:

1. 使用指针类型的map(推荐)

这是最直接的方法,map的值存储为结构体指针,可以直接修改:

package main

import "fmt"

type T struct {
    n int
}

func main() {
    mp := make(map[int]*T)
    mp[0] = &T{n: 42}
    fmt.Println(mp[0].n) // 输出 42
    
    // 直接通过指针修改
    mp[0].n = 44
    fmt.Println(mp[0].n) // 输出 44
}

2. 通过临时变量修改后重新赋值

如果必须使用值类型的map,需要先获取副本,修改后再存回:

package main

import "fmt"

type T struct {
    n int
}

func main() {
    m := make(map[int]T)
    m[0] = T{n: 42}
    fmt.Println(m[0].n) // 输出 42
    
    // 获取副本,修改,然后存回
    tmp := m[0]
    tmp.n = 44
    m[0] = tmp
    fmt.Println(m[0].n) // 输出 44
}

3. 使用包含指针字段的结构体

另一种方法是让结构体包含指针字段,这样即使结构体本身是值类型,也能通过指针间接修改:

package main

import "fmt"

type Inner struct {
    n int
}

type T struct {
    inner *Inner
}

func main() {
    m := make(map[int]T)
    m[0] = T{inner: &Inner{n: 42}}
    fmt.Println(m[0].inner.n) // 输出 42
    
    // 通过inner指针修改
    m[0].inner.n = 44
    fmt.Println(m[0].inner.n) // 输出 44
}

4. 使用sync.Map(并发安全场景)

如果需要并发访问,可以使用sync.Map:

package main

import (
    "fmt"
    "sync"
)

type T struct {
    n int
}

func main() {
    var m sync.Map
    m.Store(0, &T{n: 42})
    
    if v, ok := m.Load(0); ok {
        fmt.Println(v.(*T).n) // 输出 42
        v.(*T).n = 44
        fmt.Println(v.(*T).n) // 输出 44
    }
}

关键点说明

  1. 无法获取map元素地址的原因:Go语言规范明确规定,不能获取map元素的地址。这是因为map可能会在内存中重新组织(如扩容时),导致之前的地址失效。

  2. 性能考虑:使用指针类型的map可以减少大结构体的复制开销,但需要注意内存管理和并发安全问题。

  3. nil指针检查:使用指针类型的map时,需要确保指针不为nil:

if mp[0] != nil {
    mp[0].n = 44
}

在你的示例代码中,最后两个打印语句输出44的实现应该采用第一种或第二种方法。指针类型的map是最简洁的解决方案。

回到顶部