1.22版本的for range
我们都知道,自从go1.22版本以来,for range允许查看元素地址,但是这个地址只是临时变量的地址,我们以下面的代码为例:
func main() {
arr := [2]int{1, 2}
res := []*int{}
for i, v := range arr {
fmt.Println(i, &v)
res = append(res, &v)
}
fmt.Println(*res[0], *res[1])
}
从C语言出发
如果是C语言我们可以转化为以下代码:
for(int i = 0; i < 2; i++){
v = arr[i];
printf("%p\n",&v);
}
我们每次执行fmt.Println(&v)的时候只会输出一个地址,因为我们从始至终都只有一个变量v,不断承担着拷贝arr[i]的任务,因此输出自然只有一个,类似于早期go版本每次fmt.Println(i, &v)输出结果只有一个地址,因此最终fmt.Println(*res[0], *res[1])输出自然只能是同一个数,并且是最后赋值给v的数2。
回归Go语言(以下内容仅针对1.22版本之后)
经过测试,我们发现1.22版本居然可以正确输出结果1和2,并不是早期版本的错误输出2,2:
而且这个地址中间为什么隔着32字节?(0x20为16进制数,转化为32十进制数)
其实不难发现,我们的代码中存在一个append操作,有可能在对切片扩容的过程中转移了切片在内存中的位置,为此,我们修改代码进一步分析:
func main() {
arr := [2]int{1, 2}
res := []*int{}
for i, v := range arr {
fmt.Println(i, &v)
res = append(res, &v)
}
fmt.Println(*res[0], *res[1], unsafe.Sizeof(*res[0]), unsafe.Sizeof(res[1]))
fmt.Println("res[0]is:", res[0])
fmt.Println("res[1]is:", res[1])
fmt.Println("The address of arr[0]is:", &arr[0])
fmt.Println("The address of arr[1]is:", &arr[1])
}
我们首先输出了res每个元素(也就是地址)指向元素的长度,然后输出res数组中每个元素(地址)的长度,发现都为8字节(分别为int类型和*int类型),而后输出原数组arr地址。
结果我们发现res数组保存的1和2中间间隔48字节,并且arr[0]和arr[1]包含在这48字节中,因此,为了更进一步分析,我们将深入探索res[0]和res[1]中间包含的内容,由于GoLand中并没有类似VS一样查看内存数据的功能,并且golang不支持直接对指针进行加减乘除,因此我们只能使用unsafe库,将res[0]到res[1]中间的内容按照int(每8字节输出)。
【注】详细转换过程如下:
首先将地址经过unsafe.Pointer转化为Pointer类型,然后再转化为uintptr,这样就能进行相加减,计算完之后我们再将其强制转化为指向*int的指针即可。
for i := 0; i < 7; i++ {
//fmt.Println(*(uintptr(unsafe.Pointer(res[0])) + uintptr(i*8)))
addr := uintptr(unsafe.Pointer(res[0])) + uintptr(i*8) // 将 i*8 转换为 uintptr
fmt.Printf("Address at offset %d: %x\n", i*8, addr)
// 通过解引用来获取这个地址的值
value := *(*int)(unsafe.Pointer(addr))
fmt.Printf("Value at address %x: %d\n", addr, value)
}
最终我们得到:
结果我们发现依然有3个地址存在问题,目前推测可能是以下问题:
- 实际数据不一定是按照8字节(即int)输出
- 这里面的数据有其他意义
等以后有时间了继续查找原因。