问题:

起因是在水群的时候看到这么个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"unsafe"
)

type Reception struct {
p unsafe.Pointer
len int
}

func main() {
type A struct {
s string
}
m := make(map[string]struct{})
tmp := A{s: "hello"}
m[tmp.s] = struct{}{}
// 由于 A 结构体的字段 s 是一个字符串类型,所以在将 tmp.s 作为 map 的 key 时,会将字符串的地址作为 key,而不是字符串的值
fmt.Printf("tmp.s: %+v\n", &tmp.s)

rB := *(*Reception)(unsafe.Pointer(&tmp.s))
fmt.Printf("rB.p: %+v\n", rB.p)

for k := range m {
rA := *(*Reception)(unsafe.Pointer(&k))
fmt.Printf("rA.p: %+v\n", rA.p)
}
}

在这段代码中,将A 类型结构体的实例tmp 中的string 类型字段tmp.s 作为map类型m的key,然后在试图释放tmp 实例的时候会发现无法释放。这是因为string 类型是一个结构体,tmp.s 实际上是一个指针,所以m在将其作为key的时候是引用了该指针,这就导致tmp 无法被释放,因为它任然被m 引用。

tmp.s 的指针转换为通用指针类型,最后转为Reception 类型的指针(Reception结构体对应string类型的底层结构体实现)并输出该指针。取出m 中的key,进行同样的指针转换,最后输出,发现两个指针的内容相同,说明这两个指针指向了同一个地址。

1
2
3
tmp.s: 0x14000102020
rB.p: 0x102e9e800
rA.p: 0x102e9e800

实际上tmp.s 这个字段就是一个和Reception 结构一致的结构体,它们的第一个字段都是一个8字节的通用指针类型,第二个字段都是一个8字节的int类型(在64操作系统上默认为8字节,而在32位操作系统上默认为4个字节),所以tmp.s 会占用16个字节。由于string是Go的基础类型,直接打印只能输出字符串的值,而无法获取其字符串的真实地址,因此需要将字符串类型的结构体指针通过通用指针类型转换为自定义的Reception 结构体就可以获取到字符串内存空间的指针。

这里转换的原理其实只是修改了这16个字节的语义。本来这16个字节会被计算机解释为string 类型,但是通过将其转换为自定义的Reception 结构体后,就会按照Reception 结构体的规则来解析这16个字节,而Reception 结构体的字段结构是和string 类型一致的,这自然就拿到了我们想要的字符串的真实地址,即前八个字节解析出来的Recption.p

解决办法

至于这个问题的解决办法很简单,只需要深拷贝 一份该字符串使引用的地址不同即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"unsafe"
)

type Reception struct {
p unsafe.Pointer
len int
}

func main() {
type A struct {
s string
}
m := make(map[string]struct{})
tmp := A{s: "hello"}
// 将字符串拷贝一份,使 m 引用的地址与 tmp.s 不同即可
m[strings.Clone(tmp.s)] = struct{}{}
// 由于 A 结构体的字段 s 是一个字符串类型,所以在将 tmp.s 作为 map 的 key 时,会将字符串的地址作为 key,而不是字符串的值
fmt.Printf("tmp.s: %+v\n", &tmp.s)

rB := *(*Reception)(unsafe.Pointer(&tmp.s))
fmt.Printf("rB.p: %+v\n", rB.p)

for k := range m {
rA := *(*Reception)(unsafe.Pointer(&k))
fmt.Printf("rA.p: %+v\n", rA.p)
}
}

或者在 m 的key设置完成后,修改tmp.s的值,字符串的值修改后,由于原先的字符串地址被m 引用了,所以tmp.s 就会使用新的内存地址,这同样实现了两者地址引用的不同。