# 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. 切片的扩容机制

# 扩容规则

  1. 当容量小于 256 时:新容量 = 旧容量 × 2
  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. 总结要点

  1. 切片是引用类型,修改切片会影响底层数组
  2. 理解长度和容量的区别,合理预分配容量提升性能
  3. 注意切片共享底层数组的陷阱,避免意外修改
  4. 使用 copy 函数创建独立切片,避免内存泄漏
  5. 掌握 append 的扩容机制,理解性能影响
  6. 切片作为函数参数时的传递特性,需要返回新切片或使用指针

# 参考资源

  • Go 官方文档:https://golang.org/doc/
  • Go 语言圣经:https://books.studygolang.com/gopl-zh/
  • Effective Go:https://golang.org/doc/effective_go.html
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

ZJM 微信支付

微信支付

ZJM 支付宝

支付宝