1. slice是什么
slice是一个大小可以动态扩展的类数组的结构。它的底层通过指针引用着一个大小确定的数据,这个数组作数据存储。
当大小够用的时候引用的数组大小保持不变。当引用的数组大小不够用时,slice会申请新大小合适的数组空间,然后将数据复制到新的数组,最后slice的引用新的数组的地址。
slice的原型结构
go
type slice struct {
array unsafe.Pointer
len int
cap int
}
- 长度(len) :切片当前包含的元素个数。
- 容量(cap) :从切片的起始位置到底层数组的末尾的元素个数。
在开发中几乎都是使用slice,很少使用数组。
2. slice在解决什么问题
- 数组在编译阶段就需要给定大小,而有些数据只有在运行时才可以知道大小,会导致:
-
- 所以在编程的时候如果数组大小不够,需要重新申请内存,然后复制,消耗性能。
- 编译时申请太大的数组可能会浪费内存。
- 不知道数组实际存放了多少个数据,如果想知道,需要对数组多二封装,增加工作量。
- 数组内存共享困难,只能靠复制,如果不复制就丢失数组的特性。因为go的数组内存共享只有slice,所以这里拿C语言举例:
arduino
#include <stdio.h>
void printArray(int* arr, int len) {
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
}
int main() {
int data[3] = {1, 2, 3};
printArray(++data, 2); // 只能传指针和长度
return 0;
}
这个代码实现了内存共享,但丢失数组的大小,需要另外一个参数做说明长度的补充,所以比较繁琐。
go
package main
import "fmt"
func main() {
arr := []int{1, 2, 3, 4, 5}
s1 := arr[1:4] // s1 = [2 3 4]
s2 := arr[2:] // s2 = [3 4 5]
fmt.Println("s1:", s1)
fmt.Println("s2:", s2)
s1[0] = 99 // 修改 s1 同时影响 arr 和 s2
fmt.Println("arr:", arr)
}
内存得到了共享,没有丢失长度,而且还有容量
2.1.1. 问题总结
问题 | C语言数组(或 Go数组) | Go切片 |
---|---|---|
不确定大小 | 编译时固定 | 运行时动态增长 |
扩容麻烦 | 需手动申请 + 复制 | append 自动处理 |
长度管理 | 需额外变量维护 | len(slice) 内建支持 |
共享困难 | 只能手动传指针、复制数据 | 多个切片可共享底层数组 |
3. slice怎么解决
- 容量可以动态扩容
- 存放数据的个数可知
- 可以与数组或slice共享内存,不切不丢失长度和容量
4. slice的特性
特性 | 描述说明 |
---|---|
动态扩容 | 底层数组不足时,自动分配新数组并复制旧数据 |
共享内存 | 多个切片可共享底层数组(类似 C 中的指针),节省内存且支持原位修改 |
内建长度与容量 | 内建 len 和 cap ,不需要手动传入长度 |
轻量结构 | 仅 3 个字段,传参/赋值成本低 |
值类型 + 引用语义 | 本质是值类型,但包含指向共享底层数组的指针,因此具有引用语义 |
简化编程 | 比 C 或 Go 原生数组更灵活,适合日常开发场景 |
4.1. 轻量
在go里将一个数组传入函数参数,则会复制一份数组。在c语言下默认是引用,但丢失了数组的容量。slice是一个简单的数据结构(值类型),作为参数也会复制一份slice,但slice引用的数组不会被分支,所以成本低。在x86_64中,slice真用的内存大小为24字节。
虽然官方文档说slice是引用类型,但是它更符合值类型的特征
4.2. 容量可以动态调整
当大小够用的时候引用的数组大小保持不变。当引用的数组大小不够用时,slice会申请新大小合适的数组空间,然后将数据复制到新的数组,最后slice的引用新的数组的地址。
4.3. 保存数据的个数和容量可以随时获取
4.3.1. 1. 与 append
配合使用,扩容自动处理
go
s := []int{1, 2}
s = append(s, 3, 4)
- 若容量足够,append 就在原数组上操作;
- 若容量不足,会创建新数组,复制数据,然后返回新的切片;
- 返回的是新的切片(重要!)
4.4. 2. 切片间共享的数据是可变的,但结构是独立的
go
a := []int{1, 2, 3}
b := a[:2] // b 与 a 共享数据
b[0] = 100 // 修改 b 会影响 a
切片结构体(Data, Len, Cap)是复制的,但底层数组是共享的。
4.5. 3. slice 的 cap 和 len 是分开的
go
a := make([]int, 2, 5)
fmt.Println(len(a), cap(a)) // 2 5
len
控制能访问的元素个数cap
控制 append 时可以继续使用多少容量
这在构建 buffer 时非常有用。
4.6. 4. 支持 nil 切片和空切片
go
var a []int // nil 切片,a == nil
b := make([]int, 0) // 空切片,b != nil
- 都
len == 0
nil
切片常用于表示"没有值"- 空切片常用于表示"值是空的集合"
4.7. 5. 切片是安全的内存视图
即使多个切片共享内存,但你无法通过切片访问超出 len
范围的数据(否则 panic),这提高了安全性。
4.8. 6. 切片可以截取切片
你可以对一个切片继续切片(称为"re-slice"):
go
s := []int{1, 2, 3, 4}
s2 := s[1:3] // [2 3]
s3 := s2[1:] // [3]
4.9. 7. slice 与 copy 函数配合做深复制
go
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)
适用于避免共享底层数据的场景。
4.10. 8. 可以与 range
安全配合迭代
css
for i, v := range slice {
// i 是索引,v 是值拷贝
}
这是切片的标准使用模式之一。
4.11. 🔚 总结:slice 的附加特性(补充点)
特性名称 | 描述 |
---|---|
append 自动扩容 | 不够就重新分配 |
re-slice 支持 | 切片还能继续切 |
copy 支持 | 可用于深拷贝 |
range 安全迭代 | 配合 range 可读性高 |
nil/空区分 | nil 表示无,空表示空集合 |
cap 控制预分配 | 可优化性能,减少扩容次数 |
安全边界限制 | 超过 len 会 panic,防止越界 |
可以表示动态数组 | 在 Go 中承担动态数组的角色 |