在Go语言中,切片是引用类型,其内部结构包含指向底层数组的指针、长度和容量三个部分。判断两个切片是否引用同一内存起始地址,核心就是对比两个切片指向的底层数组的起始指针是否一致。

切片的内部结构
Go语言的切片在运行时对应的结构是reflect.SliceHeader,定义如下:
type SliceHeader struct {
Data uintptr // 指向底层数组的起始内存地址
Len int // 切片长度
Cap int // 切片容量
}其中Data字段就是我们要对比的内存起始地址值,只要两个切片的Data字段值相同,就说明它们引用了同一个底层数组的起始地址。
判断方法一:使用reflect包转换对比
可以通过reflect.SliceHeader将切片转换为对应的结构,然后对比Data字段的值,这是最常用的方法。
package main
import (
"fmt"
"reflect"
"unsafe"
)
// 判断两个切片是否引用同一内存起始地址
func isSameSliceMemory(s1, s2 []int) bool {
// 将切片转换为SliceHeader,获取底层数组指针
hdr1 := (*reflect.SliceHeader)(unsafe.Pointer(&s1))
hdr2 := (*reflect.SliceHeader)(unsafe.Pointer(&s2))
return hdr1.Data == hdr2.Data
}
func main() {
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[:]
s2 := arr[:3]
s3 := make([]int, 3)
fmt.Println("s1和s2是否同一内存起始地址:", isSameSliceMemory(s1, s2)) // true,都引用arr的底层数组
fmt.Println("s1和s3是否同一内存起始地址:", isSameSliceMemory(s1, s3)) // false,s3是新分配的底层数组
}这种方法需要注意使用unsafe.Pointer进行指针转换,使用前要确保了解相关风险,且只能用于切片类型的判断。
判断方法二:对比切片的第一个元素地址
如果两个切片都不为空,也可以通过对比它们第一个元素的地址来判断,因为第一个元素的地址就是底层数组的起始地址(如果切片从底层数组开头截取的话)。
package main
import "fmt"
func isSameMemoryByFirstElem(s1, s2 []int) bool {
// 空切片没有底层数组,直接返回false
if len(s1) == 0 || len(s2) == 0 {
return false
}
// 对比第一个元素的地址
return &s1[0] == &s2[0]
}
func main() {
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:3]
s2 := arr[1:4]
s3 := arr[:]
fmt.Println("s1和s2是否同一内存起始地址:", isSameMemoryByFirstElem(s1, s2)) // true,都从arr的索引1开始
fmt.Println("s1和s3是否同一内存起始地址:", isSameMemoryByFirstElem(s1, s3)) // false,s3从arr的索引0开始
}这种方法的局限性是如果切片是截取自底层数组的非起始位置,或者切片为空,判断结果可能不符合预期,适用场景比第一种方法更窄。
注意事项
- 两个切片长度、容量不同但
Data指针相同,依然属于引用同一内存起始地址,只是可访问的范围不同。 - 使用
append操作如果触发了底层数组扩容,切片会指向新的内存地址,此时和原切片就不再是同一内存起始地址。 - 空切片的
Data指针通常为0,两个空切片如果都是未初始化的,可能Data都为0,但这种情况不代表它们引用了同一个有效的底层数组。
根据实际场景选择合适的方法,如果需要准确判断底层数组起始地址,优先使用第一种基于reflect.SliceHeader的方法。