Golang中的切片是一种常用的数据结构,它的底层依赖数组实现,而指针是连接切片和底层数组的关键。理解指针和切片扩容的关系,能帮助开发者更清晰地掌握切片的内存变化逻辑,避免开发中出现数据不符合预期的情况。

切片的基础结构
Golang的切片在运行时对应reflect.SliceHeader结构,包含三个核心字段:指向底层数组的指针、切片的长度、切片的容量。我们可以通过下面的代码查看切片的结构定义:
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
// 定义一个切片
s := make([]int, 2, 4)
// 获取切片的运行时结构
sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&s))
fmt.Printf("指针地址: %v, 长度: %d, 容量: %dn", sliceHeader.Data, sliceHeader.Len, sliceHeader.Cap)
}
从这个结构可以看出,切片的指针直接指向底层数组的首地址,长度和容量决定了切片能访问的元素范围和可扩展的空间。
切片扩容的触发条件
当切片的长度超过当前容量时,就会触发扩容操作。比如我们往一个容量已满的切片中追加元素,就会触发扩容:
package main
import "fmt"
func main() {
// 初始化切片,长度2,容量2
s := make([]int, 2, 2)
fmt.Printf("扩容前: 指针%p, 长度%d, 容量%dn", s, len(s), cap(s))
// 追加元素,触发扩容
s = append(s, 1)
fmt.Printf("扩容后: 指针%p, 长度%d, 容量%dn", s, len(s), cap(s))
}
运行上述代码可以看到,扩容后切片的指针地址发生了变化,这说明扩容后切片指向了新的底层数组。
扩容的底层逻辑
Golang的切片扩容逻辑在src/runtime/slice.go的growslice函数中实现,核心规则如下:
- 如果当前切片的容量小于256,扩容后的新容量会翻倍
- 如果当前切片的容量大于等于256,新容量会按照当前容量的1.25倍增长
- 扩容后会申请一块新的内存空间,将原有数组的元素拷贝到新数组中,然后让切片的指针指向新的数组
我们可以通过下面的代码验证扩容的容量变化规律:
package main
import "fmt"
func main() {
s := make([]int, 2, 2)
fmt.Printf("初始容量: %dn", cap(s))
s = append(s, 1)
fmt.Printf("第一次扩容后容量: %dn", cap(s))
s = append(s, 2, 3, 4)
fmt.Printf("第二次扩容后容量: %dn", cap(s))
}
指针与扩容的关系
切片的指针和扩容的核心关联在于:扩容会导致切片指向的底层数组发生变化,因此切片的指针地址也会随之改变。如果我们在扩容前获取了切片底层数组的指针,扩容后这个指针就指向了旧的数组,和新的切片不再关联。
下面的例子演示了这种场景:
package main
import (
"fmt"
"unsafe"
)
func main() {
s := make([]int, 2, 2)
// 获取扩容前切片的指针
oldPtr := unsafe.Pointer(&s[0])
fmt.Printf("扩容前指针: %vn", oldPtr)
// 触发扩容
s = append(s, 1)
newPtr := unsafe.Pointer(&s[0])
fmt.Printf("扩容后指针: %vn", newPtr)
// 判断两个指针是否相同
fmt.Printf("指针是否相同: %vn", oldPtr == newPtr)
}
运行结果会显示扩容前后的指针不同,说明扩容后切片已经指向了新的内存地址。
注意事项
在实际开发中需要注意以下几点:
- 不要长期持有切片底层数组的指针,避免扩容后指针失效
- 如果需要切片扩容后仍然保持指针不变,可以提前设置足够的容量,避免触发扩容
- 切片作为参数传递时,传递的是切片头的拷贝,修改切片的长度和容量不会影响原切片,但如果扩容导致指针变化,原切片和新切片的底层数组会分离
我们可以通过下面的代码理解切片传参的情况:
package main
import "fmt"
func modifySlice(s []int) {
s = append(s, 1)
fmt.Printf("函数内: 指针%p, 长度%d, 容量%dn", s, len(s), cap(s))
}
func main() {
s := make([]int, 1, 1)
fmt.Printf("函数外初始: 指针%p, 长度%d, 容量%dn", s, len(s), cap(s))
modifySlice(s)
fmt.Printf("函数外最终: 指针%p, 长度%d, 容量%dn", s, len(s), cap(s))
}
上述代码中,函数内扩容后切片指针变化,函数外的原切片没有受到影响,这就是因为切片传参是值拷贝,扩容后函数内的切片指向了新的数组。