切片原理 #
一、切片底层结构 #
1.1 slice结构体 #
切片在运行时表示为:
go
type slice struct {
ptr unsafe.Pointer // 指向底层数组的指针
len int // 长度
cap int // 容量
}
1.2 内存布局 #
text
切片变量
┌─────────────────────────────────┐
│ ptr ──────┐ len │ cap │
└───────────┼─────────────────────┘
│
▼
底层数组
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │
└───┴───┴───┴───┴───┴───┴───┴───┘
↑ ↑
│ │
└─ 切片范围 ─────┘
len=4, cap=6
1.3 示例 #
go
s := make([]int, 3, 6)
fmt.Printf("len=%d, cap=%d\n", len(s), cap(s))
s = append(s, 1, 2)
fmt.Printf("len=%d, cap=%d\n", len(s), cap(s))
二、创建切片 #
2.1 字面量创建 #
go
s := []int{1, 2, 3}
// 底层:创建数组,切片指向它
2.2 make创建 #
go
s := make([]int, 3, 6)
// 底层:分配数组,创建切片
2.3 从数组切片 #
go
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4]
// 底层:切片指向arr的子区间
2.4 切片表达式 #
go
s := []int{1, 2, 3, 4, 5}
s1 := s[1:3] // len=2, cap=4
s2 := s[1:3:3] // len=2, cap=2
三、扩容机制 #
3.1 扩容触发 #
当len == cap时,append触发扩容:
go
s := make([]int, 0, 2)
s = append(s, 1, 2) // len=2, cap=2
s = append(s, 3) // 触发扩容
3.2 扩容规则 #
Go 1.18+ 扩容规则:
- 如果新容量 > 2倍旧容量,直接使用新容量
- 如果旧容量 < 256,新容量 = 2倍旧容量
- 如果旧容量 >= 256,新容量 = 旧容量 + (旧容量+3*256)/4
3.3 扩容示例 #
go
s := make([]int, 0)
for i := 0; i < 20; i++ {
s = append(s, i)
fmt.Printf("len=%d, cap=%d\n", len(s), cap(s))
}
// 输出:
// len=1, cap=1
// len=2, cap=2
// len=3, cap=4
// len=4, cap=4
// len=5, cap=8
// len=6, cap=8
// ...
3.4 扩容过程 #
go
func growslice(oldSlice, newLen) {
newCap := calculateNewCap(oldSlice.cap, newLen)
newArray := malloc(newCap * elementSize)
copy(newArray, oldSlice.ptr, oldSlice.len)
return slice{ptr: newArray, len: newLen, cap: newCap}
}
四、内存共享 #
4.1 切片共享底层数组 #
go
s1 := []int{1, 2, 3, 4, 5}
s2 := s1[1:3] // 共享底层数组
s2[0] = 20
fmt.Println(s1) // [1 20 3 4 5]
4.2 切片表达式与容量 #
go
s1 := []int{1, 2, 3, 4, 5}
s2 := s1[1:3] // len=2, cap=4
// s2可以访问s1[3]和s1[4](通过扩容)
s2 = append(s2, 30, 40)
fmt.Println(s1) // [1 2 3 30 40](修改了s1)
4.3 限制容量避免共享 #
go
s1 := []int{1, 2, 3, 4, 5}
s2 := s1[1:3:3] // len=2, cap=2
s2 = append(s2, 30) // 扩容,新数组
fmt.Println(s1) // [1 2 3 4 5](未改变)
五、性能优化 #
5.1 预分配容量 #
go
// 不好:频繁扩容
var s []int
for i := 0; i < 10000; i++ {
s = append(s, i)
}
// 好:预分配
s := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
s = append(s, i)
}
5.2 避免不必要的复制 #
go
// 不好:复制整个切片
func process(s []int) []int {
result := make([]int, len(s))
copy(result, s)
// 处理result...
return result
}
// 好:直接使用
func process(s []int) {
// 直接处理s
}
5.3 切片作为缓冲区 #
go
var buf [1024]byte
n, _ := conn.Read(buf[:])
process(buf[:n])
5.4 重用切片 #
go
var pool = sync.Pool{
New: func() interface{} {
return make([]byte, 1024)
},
}
func process() {
buf := pool.Get().([]byte)
defer pool.Put(buf)
// 使用buf...
}
六、内存泄漏 #
6.1 切片引用导致泄漏 #
go
func leak() []int {
arr := [10000]int{1, 2, 3, ...}
return arr[:10] // 返回的切片引用整个数组
}
6.2 解决方法 #
go
func noLeak() []int {
arr := [10000]int{1, 2, 3, ...}
result := make([]int, 10)
copy(result, arr[:10])
return result // 只复制需要的部分
}
6.3 删除元素泄漏 #
go
type Data struct {
Value int
Info []byte
}
func remove(s []*Data, i int) []*Data {
s[i] = nil // 置nil,允许GC回收
return append(s[:i], s[i+1:]...)
}
七、切片与数组转换 #
7.1 数组转切片 #
go
arr := [5]int{1, 2, 3, 4, 5}
s := arr[:] // 切片引用数组
7.2 切片转数组 #
go
s := []int{1, 2, 3, 4, 5}
arr := [5]int(s) // Go 1.20+
7.3 切片转数组指针 #
go
s := []int{1, 2, 3, 4, 5}
arr := (*[5]int)(s) // 共享底层数据
八、常见陷阱 #
8.1 append可能改变底层数组 #
go
s1 := []int{1, 2, 3}
s2 := s1
s1 = append(s1, 4) // 可能不扩容
fmt.Println(s2) // [1 2 3] 或 [1 2 3 4]
s1 = append(s1, 5, 6, 7) // 扩容
fmt.Println(s2) // [1 2 3](未改变)
8.2 切片作为函数参数 #
go
func appendValue(s []int) {
s = append(s, 1) // 可能改变s的指针
}
func main() {
s := []int{1, 2, 3}
appendValue(s)
fmt.Println(s) // 可能是 [1 2 3] 或 [1 2 3 1]
}
8.3 nil切片 vs 空切片 #
go
var s1 []int // nil切片
s2 := []int{} // 空切片
s3 := make([]int, 0) // 空切片
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
fmt.Println(s3 == nil) // false
九、最佳实践 #
9.1 预分配容量 #
go
s := make([]int, 0, expectedSize)
9.2 使用完整切片表达式 #
go
s := data[low:high:max] // 限制容量
9.3 避免切片引用大数组 #
go
// 复制需要的数据
result := make([]int, len(s))
copy(result, s)
9.4 注意函数返回切片 #
go
// 返回新切片
func process(s []int) []int {
result := make([]int, len(s))
// ...
return result
}
十、总结 #
切片原理要点:
| 特性 | 说明 |
|---|---|
| 结构 | ptr + len + cap |
| 扩容 | 2倍或约25%增长 |
| 共享 | 切片共享底层数组 |
| 泄漏 | 注意引用导致泄漏 |
关键点:
- 底层结构:指针、长度、容量
- 扩容机制:2倍或约25%增长
- 内存共享:切片共享底层数组
- 内存泄漏:切片引用可能导致泄漏
- 性能优化:预分配容量,避免频繁扩容
准备好学习映射(Map)了吗?让我们进入下一章!
最后更新:2026-03-26