关于Go的map无法对结构体值局部赋值的探究
Kale

在写项目过程中,遇到一个问题,即gomap如果value是结构体时,无法对结构体进行局部更新

即:

1
2
3
4
5
6
7
8
9
type demo struct {
a int
b int
}
func main() {
var m = make(map[string]demo)
m["a"] = demo{1,2}
m["a"].a = 3
}

此代码无法通过编译,报错cannot assign to struct field m["a"].a in map
但是对m["a"]进行一整个结构体的赋值则是允许的,在java中是可以局部赋值的,但是go中却不允许,很奇怪,于是查找解决方案。

map底层结构

首先讲map的底层结构,不同于cppgomap底层是hash表,初始化map时,就初始化了一个hmap,作为该map的头

1
2
3
4
5
6
7
8
9
10
11
12
type hmap struct {
count int
flags uint8
B uint8
noverflow uint16
hash0 uint32

buckets unsafe.Pointer
oldbuckets unsafe.Pointer
nevacuate uintptr
extra *mapextra
}

以上是hmap的结构体定义,关于各个值代表的含义网上有很多介绍,这里就不赘述。

hmap不存储具体的keyvaluekeyvaluebucket存储,一个bucket可以存储8个key和8个value,其中key排列在一起,value排列在一起,这样的好处就是在keyvalue的长度不一致时,可以避免空间浪费。

map增加元素的过程中,可能会出现空间不够用的情况,这个时候就会进行扩容,这里比较不同的是,为了防止扩容带来的性能抖动,go选择了渐进式扩容,也就是在一次删除或者更新操作中不全部扩容完毕,而是进行一部分的扩容,扩容会将空间扩展为之前的两倍。

hmap的字段中,buckets就是新桶数组的指针,oldbuckets就是旧桶数组的指针。底层就是数组+链表。

hmap中定义了一个extra字段,extra字段也是一个结构体:

1
2
3
4
5
6
type mapextra struct {
overflow *[]*bmap
oldoverflow *[]*bmap

nextOverflow *bmap
}

其中overflow是一个切片,在map的设计中,假如分配了2^B - 1个bucket,也会分配2^B - 1overflowBucket,当bucket装满时,就链接到一个overflowBucket进行扩展。当overflow快存满时,也会进行扩容,这个时候不会引起翻倍扩容,而是等额扩容,因为此时很可能是进行了大量删除工作,相当于给内存做一次整理。

问题原因

前面提到,map的更新等操作是可能会引起扩容的,所以内存地址会变。

  1. 通过m["a"].a = 3这种操作进行更新,如果不存在m["a"]值,则会引起插入操作,这时没有一个完整结构体,可能会导致问题,如果存在,扩容时,value会在内存中移动,旧的指针地址在map改变时会无效,所以为了安全考虑,go禁止这样寻址,两个原因,如果对象不存在,则返回零值,零值是不可变对象,所以不能寻址,如果对象存在,因为gomap实现中元素的地址是变化的,这意味着寻址的结果是无意义的。
  2. golang中没有引用类型,通过m["a"].a取出来的是一份拷贝,修改这里的拷贝,实际修改的内容没有复制回原来的struct

解决方案

java不同,java中存在引用类型,所以可以通过那种方式进行修改,就算会扩容,修改引用,原始数据总是跟着变。
解决方案有两种,一是进行全局赋值,例如:

1
2
3
tmp := m["a"]
tmp.a = 333
m["a"] = tmp

这样可行的原因是相当于给m["a"]赋新值了,不管m["a"]存不存在都不会出现问题。

或者将value改为结构体指针:

1
2
3
4
var m = make(map[string]*demo)
m["a"] = &demo{1, 2}
m["a"].a = 333
fmt.Println(m["a"])

这样在map中存储的就是结构体的指针,获取到的拷贝,也是对应地址,所以自然可以修改成功,不管map再怎么扩容,key对应的value总是不变的。

  • 本文标题:关于Go的map无法对结构体值局部赋值的探究
  • 本文作者:Kale
  • 创建时间:2021-04-07 15:42:44
  • 本文链接:https://kalew515.com/2021/04/07/关于Go的map无法对结构体值局部赋值的探究/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!