在Go语言中,切片是一种动态长度的序列类型,底层依赖数组存储元素。当我们对切片的元素取地址得到指针后,这个指针是否会始终指向同一个内存位置,需要结合切片的实际运行情况分析。

切片未发生扩容时的指针情况
当切片的长度没有超过当前的容量时,切片不会触发扩容操作,底层数组不会被替换,此时对切片元素取地址得到的指针会一直指向同一个内存位置。
我们可以通过下面的代码验证这个场景:
package main
import "fmt"
func main() {
// 初始化切片,长度和容量都为3
s := make([]int, 3, 3)
s[0] = 10
s[1] = 20
s[2] = 30
// 获取第一个元素的指针
p := &s[0]
fmt.Printf("初始指针指向的地址: %p, 对应值: %dn", p, *p)
// 修改第一个元素的值,不触发扩容
s[0] = 100
fmt.Printf("修改元素后指针指向的地址: %p, 对应值: %dn", p, *p)
// 向切片追加元素,不超过容量,不会扩容
s = append(s, 40)
fmt.Printf("追加未超容元素后指针指向的地址: %p, 对应值: %dn", p, *p)
}
运行上述代码,三次打印的指针地址都是相同的,说明在未发生扩容的情况下,切片元素的地址是稳定的,指针始终指向同一个内存位置。
切片发生扩容时的指针情况
当切片追加元素后长度超过当前容量时,Go运行时会触发切片的扩容机制,此时会创建一个新的底层数组,把原有元素复制到新数组中,原有切片的底层数组会被替换。这种情况下,之前获取的元素指针仍然指向旧的底层数组地址,而切片的元素已经存储在新数组中,指针就不再指向切片元素对应的内存位置了。
我们通过下面的代码验证这个场景:
package main
import "fmt"
func main() {
// 初始化切片,长度为3,容量为3
s := make([]int, 3, 3)
s[0] = 10
s[1] = 20
s[2] = 30
// 获取第一个元素的指针
p := &s[0]
fmt.Printf("初始指针指向的地址: %p, 对应值: %dn", p, *p)
// 追加元素超过容量,触发扩容
s = append(s, 40, 50)
fmt.Printf("触发扩容后切片元素地址: %pn", &s[0])
fmt.Printf("旧指针指向的地址: %p, 对应值: %dn", p, *p)
}
运行代码后可以发现,扩容后切片第一个元素的地址已经和之前的指针地址不同,旧指针指向的还是原来底层数组的地址,而切片的元素已经存储在新数组中,此时指针就不再指向切片的对应元素了。
结论总结
Go中对切片元素取地址得到的指针是否始终指向同一内存位置,取决于切片是否发生扩容:
- 如果切片没有发生扩容,底层数组不变,指针会一直指向同一个内存位置,和切片元素保持对应。
- 如果切片发生扩容,底层数组会被替换,旧指针仍然指向原来的旧数组地址,不再和切片的新元素对应。
因此在开发中,如果需要长期持有切片元素的指针,需要注意切片的扩容情况,避免因为扩容导致指针失效的问题。如果无法避免切片扩容,建议在每次扩容后重新获取元素的指针。