# Go 语言切片 (Slice) 详解学习笔记
# 1. 切片简介
# 什么是切片
- 切片 (Slice) 是 Go 语言中一个非常重要的数据结构
- 可以看作是对数组的抽象和封装
- 提供了比数组更灵活的数据操作方式
- 类似于其他编程语言中的动态数组
# 切片 vs 数组的区别
| 特性 | 数组 | 切片 |
|---|---|---|
| 长度 | 固定长度 | 动态长度 |
| 类型 | 值类型 | 引用类型 |
| 内存 | 存储实际数据 | 存储指向底层数组的指针 |
| 扩容 | 不支持 | 支持动态扩容 |
# 2. 切片的底层结构
# 内部数据结构
type slice struct { | |
array unsafe.Pointer // 指向底层数组的指针 | |
len int // 切片长度 | |
cap int // 切片容量 | |
} |
# 三个重要属性
- 指针 (Pointer): 指向底层数组中切片开始位置的元素
- 长度 (Length): 切片中元素的个数
- 容量 (Capacity): 从切片开始位置到底层数组末尾的元素个数
# 3. 切片的创建方式
# 3.1 使用 make 函数
// 创建长度为 5,容量为 10 的切片 | |
// 切片长度一旦被指定了,就代表对应位置已经被分配了元素,设置的是对应元素类型的零值,所以长度为 5 的切片,对应位置的元素都是对应类型的零值 | |
s1 := make([]int, 5, 10) | |
// 创建长度和容量都为 5 的切片 | |
s2 := make([]int, 5) |
# 3.2 使用切片字面量
// 直接初始化 | |
s3 := []int{1, 2, 3, 4, 5} | |
// 空切片 | |
s4 := []int{} |
# 3.3 从数组或切片创建
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} | |
// 切片操作 [start:end],左闭右开 | |
// 本质上是一次引用传递操作,因为不论如何截取,底层复用的都是同一块内存空间中的数据 | |
s5 := arr[2:7] // 从索引 2 到 6(不包含 7) | |
s6 := arr[:5] // 从开始到索引 4 | |
s7 := arr[3:] // 从索引 3 到末尾 | |
s8 := arr[:] // 全部元素 |
对于切片的
切片初始化源码
位于 runtime/slice.go 文件中
func makeslice(et *_type, len, cap int) unsafe.Pointer { | |
// 根据 cap 结合每个元素的大小,计算出消耗的总容量 | |
mem, overflow := math.MulUintptr(et.Size_, uintptr(cap)) | |
if overflow || mem > maxAlloc || len < 0 || len > cap { | |
// 如果容量超限,len 取负值或者 len 超过 cap,直接 panic | |
mem, overflow := math.MulUintptr(et.Size_, uintptr(len)) | |
if overflow || mem > maxAlloc || len < 0 { | |
panicmakeslicelen() | |
} | |
panicmakeslicecap() | |
} | |
// 调用 mallocgc 进行内存分配以及切片初始化 | |
// runtime/malloc.go | |
return mallocgc(mem, et, true) | |
} |
# 4. 切片的常用操作
# 4.1 获取长度和容量
s := make([]int, 5, 10) | |
fmt.Println(len(s)) // 长度: 5 | |
fmt.Println(cap(s)) // 容量: 10 |
# 4.2 添加元素 (append)
s := []int{1, 2, 3} | |
s = append(s, 4) // 添加单个元素 | |
s = append(s, 5, 6, 7) // 添加多个元素 | |
s2 := []int{8, 9, 10} | |
s = append(s, s2...) // 添加另一个切片 |
# 4.3 复制切片 (copy)
src := []int{1, 2, 3, 4, 5} | |
dst := make([]int, len(src)) | |
copy(dst, src) // 将 src 复制到 dst |
# 5. 切片的扩容机制
# 扩容规则
- 当容量小于 256 时:新容量 = 旧容量 × 2
- 当容量大于等于 256 时:新容量 = 旧容量 × 1.25
# 扩容示例
func demonstrateGrowth() { | |
s := make([]int, 0, 1) | |
for i := 0; i < 10; i++ { | |
fmt.Printf("len: %d, cap: %d\n", len(s), cap(s)) | |
s = append(s, i) | |
} | |
} |
# 核心步骤
- 如果扩容后的新容量小于原切片的容量,则 panic
- 如果切片元素大小为 0(元素类型为 struct {}), 则直接服用一个全局的 zerobase 实例,直接返回
- 如果预期的新容量超过老容量的两倍,则直接采用预期的新容量
- 如果老容量小于 256,则直接采用老容量的 2 倍作为新容量
- 如果老容量已经大于等于 256,则在老容量的基础上扩容
- 结合
mallocgc流程中,对内存分配单元 mspan 的等级制度,推算得到实际需要申请的内存空间大小 - 调用 mallocgc,对新切片进行内存初始化
- 调用 memmove 方法,将老切片中的内容拷贝到新切片中
- 返回扩容后的新切片
# 源码分析 runtime/slice.go -> growslice()
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice { | |
oldLen := newLen - num | |
if newLen < 0 { | |
panic(errorString("growslice: len out of range")) | |
} | |
if et.Size_ == 0 { | |
// 如果元素大小为 0,则无需分配空间直接返回 | |
return slice{unsafe.Pointer(&zerobase), newLen, newLen} | |
} | |
newcap := nextslicecap(newLen, oldCap) | |
var overflow bool | |
var lenmem, newlenmem, capmem uintptr | |
// Specialize for common values of et.Size. | |
// For 1 we don't need any division/multiplication. | |
// For goarch.PtrSize, compiler will optimize division/multiplication into a shift by a constant. | |
// For powers of 2, use a variable shift. | |
noscan := !et.Pointers() | |
switch { | |
case et.Size_ == 1: | |
lenmem = uintptr(oldLen) | |
newlenmem = uintptr(newLen) | |
capmem = roundupsize(uintptr(newcap), noscan) | |
overflow = uintptr(newcap) > maxAlloc | |
newcap = int(capmem) | |
case et.Size_ == goarch.PtrSize: | |
lenmem = uintptr(oldLen) * goarch.PtrSize | |
newlenmem = uintptr(newLen) * goarch.PtrSize | |
capmem = roundupsize(uintptr(newcap)*goarch.PtrSize, noscan) | |
overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize | |
newcap = int(capmem / goarch.PtrSize) | |
case isPowerOfTwo(et.Size_): | |
var shift uintptr | |
if goarch.PtrSize == 8 { | |
// Mask shift for better code generation. | |
shift = uintptr(sys.TrailingZeros64(uint64(et.Size_))) & 63 | |
} else { | |
shift = uintptr(sys.TrailingZeros32(uint32(et.Size_))) & 31 | |
} | |
lenmem = uintptr(oldLen) << shift | |
newlenmem = uintptr(newLen) << shift | |
capmem = roundupsize(uintptr(newcap)<<shift, noscan) | |
overflow = uintptr(newcap) > (maxAlloc >> shift) | |
newcap = int(capmem >> shift) | |
capmem = uintptr(newcap) << shift | |
default: | |
lenmem = uintptr(oldLen) * et.Size_ | |
newlenmem = uintptr(newLen) * et.Size_ | |
capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap)) | |
capmem = roundupsize(capmem, noscan) | |
newcap = int(capmem / et.Size_) | |
capmem = uintptr(newcap) * et.Size_ | |
} | |
// The check of overflow in addition to capmem > maxAlloc is needed | |
// to prevent an overflow which can be used to trigger a segfault | |
// on 32bit architectures with this example program: | |
// | |
// type T [1<<27 + 1]int64 | |
// | |
// var d T | |
// var s []T | |
// | |
// func main() { | |
// s = append(s, d, d, d, d) | |
// print(len(s), "\n") | |
// } | |
if overflow || capmem > maxAlloc { | |
panic(errorString("growslice: len out of range")) | |
} | |
var p unsafe.Pointer | |
// 非指针类型 | |
if !et.Pointers() { | |
p = mallocgc(capmem, nil, false) | |
} else { | |
// 指针类型 | |
p = mallocgc(capmem, et, true) | |
} | |
} | |
// 将切片的内容拷贝到库容后的位置 p | |
memmove(p, oldPtr, lenmem) | |
return slice{p, newLen, newcap} | |
} |
nextslicecap()
func nextslicecap(newLen, oldCap int) int { | |
newcap := oldCap | |
doublecap := newcap + newcap | |
if newLen > doublecap { | |
return newLen | |
} | |
const threshold = 256 | |
if oldCap < threshold { | |
return doublecap | |
} | |
for { | |
// Transition from growing 2x for small slices | |
// to growing 1.25x for large slices. This formula | |
// gives a smooth-ish transition between the two. | |
newcap += (newcap + 3*threshold) >> 2 | |
// We need to check `newcap >= newLen` and whether `newcap` overflowed. | |
// newLen is guaranteed to be larger than zero, hence | |
// when newcap overflows then `uint(newcap) > uint(newLen)`. | |
// This allows to check for both with the same comparison. | |
if uint(newcap) >= uint(newLen) { | |
break | |
} | |
} | |
// Set newcap to the requested cap when | |
// the newcap calculation overflowed. | |
if newcap <= 0 { | |
return newLen | |
} | |
return newcap | |
} |
# 6. 切片使用中的常见陷阱
# 6.1 切片共享底层数组
arr := [5]int{1, 2, 3, 4, 5} | |
s1 := arr[1:4] // [2, 3, 4] | |
s2 := arr[2:5] // [3, 4, 5] | |
s1[1] = 999 // 修改 s1 会影响 s2 | |
fmt.Println(s2) // [999, 4, 5] |
# 6.2 切片作为函数参数
// 错误示例:无法改变切片本身 | |
func wrongAppend(s []int, val int) { | |
s = append(s, val) // 只是修改了局部变量 | |
} | |
// 正确示例:返回新切片 | |
func correctAppend(s []int, val int) []int { | |
return append(s, val) | |
} | |
// 或使用指针 | |
func appendWithPointer(s *[]int, val int) { | |
*s = append(*s, val) | |
} |
# 6.3 内存泄漏风险
// 潜在内存泄漏 | |
func getFirstThree(s []int) []int { | |
return s[:3] // 仍然引用整个底层数组 | |
} | |
// 安全做法 | |
func getFirstThreeSafe(s []int) []int { | |
result := make([]int, 3) | |
copy(result, s[:3]) | |
return result | |
} |
# 7. 切片的高级用法
# 7.1 二维切片
// 创建二维切片 | |
matrix := make([][]int, 3) | |
for i := range matrix { | |
matrix[i] = make([]int, 4) | |
} | |
// 或直接初始化 | |
matrix2 := [][]int{ | |
{1, 2, 3}, | |
{4, 5, 6}, | |
{7, 8, 9}, | |
} |
# 7.2 切片排序
import "sort" | |
s := []int{3, 1, 4, 1, 5, 9} | |
sort.Ints(s) // 排序整数切片 | |
// 自定义排序 | |
sort.Slice(s, func(i, j int) bool { | |
return s[i] > s[j] // 降序排列 | |
}) |
# 7.3 切片去重
func removeDuplicates(s []int) []int { | |
seen := make(map[int]bool) | |
result := []int{} | |
for _, val := range s { | |
if !seen[val] { | |
seen[val] = true | |
result = append(result, val) | |
} | |
} | |
return result | |
} |
# 8. 性能优化建议
# 8.1 预分配容量
// 不好的做法 | |
var s []int | |
for i := 0; i < 1000; i++ { | |
s = append(s, i) // 多次扩容 | |
} | |
// 好的做法 | |
s := make([]int, 0, 1000) // 预分配容量 | |
for i := 0; i < 1000; i++ { | |
s = append(s, i) // 无需扩容 | |
} |
# 8.2 避免不必要的拷贝
// 使用切片而非数组作为参数 | |
func processSlice(s []int) { | |
// 处理切片,避免拷贝大量数据 | |
} |
# 9. 实践练习题
# 练习 1:实现切片插入函数
func insert(s []int, index int, value int) []int { | |
// 在指定位置插入元素 | |
s = append(s, 0) | |
copy(s[index+1:], s[index:]) | |
s[index] = value | |
return s | |
} |
# 练习 2:实现切片删除函数
func remove(s []int, index int) []int { | |
// 删除指定位置的元素 | |
return append(s[:index], s[index+1:]...) | |
} |
# 练习 3:切片反转
func reverse(s []int) { | |
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { | |
s[i], s[j] = s[j], s[i] | |
} | |
} |
# 10. 总结要点
- 切片是引用类型,修改切片会影响底层数组
- 理解长度和容量的区别,合理预分配容量提升性能
- 注意切片共享底层数组的陷阱,避免意外修改
- 使用 copy 函数创建独立切片,避免内存泄漏
- 掌握 append 的扩容机制,理解性能影响
- 切片作为函数参数时的传递特性,需要返回新切片或使用指针
# 参考资源
- Go 官方文档:https://golang.org/doc/
- Go 语言圣经:https://books.studygolang.com/gopl-zh/
- Effective Go:https://golang.org/doc/effective_go.html