Golang的切片Slice

Go 语言切片(Slice)笔记

1. 切片概述

切片(Slice)是 Go 语言中一种灵活、动态的数据结构,类似于其他语言中的"可变长数组"。它建立在数组之上,提供了更方便、更安全的序列操作。切片的特点包括:

  • 动态长度 :切片的长度(len)可以在运行时改变。

  • 容量管理 :切片有容量(cap)概念,容量大于等于长度,当长度超过容量时会自动扩容。

  • 引用类型:切片本身是一个引用类型,传递时共享底层数组。

  • 连续内存:切片元素存储在连续的内存空间中,支持高效的随机访问。

2. 切片数据结构

切片在运行时由一个称为 slice header 的结构体表示,定义在 runtime/slice.go 中:

复制代码
type slice struct {
    array unsafe.Pointer // 指向底层数组的指针
    len   int            // 切片当前长度
    cap   int            // 切片容量
}
  • array:指向底层数组的起始地址。

  • len:切片中实际存储的元素个数,索引范围 [0, len) 可访问。

  • cap:从 array 开始到底层数组末尾的总容量,索引范围 [0, cap) 已分配内存。

3. 创建和初始化切片

3.1 声明但未初始化

复制代码
var s []int // s == nil,没有分配底层数组

3.2 使用字面量初始化

复制代码
s := []int{1, 2, 3} // len = 3, cap = 3

3.3 使用 make 创建

复制代码
s := make([]int, 5)      // len = 5, cap = 5,元素初始化为零值
s := make([]int, 5, 10)  // len = 5, cap = 10,预留额外空间
  • 必须保证 len <= cap,否则编译错误或运行时 panic。

  • 当仅指定长度时,容量默认等于长度。

3.4 从数组或切片创建(截取)

复制代码
arr := [5]int{1,2,3,4,5}
s := arr[1:4]            // 从数组创建,s = [2,3,4], len=3, cap=4 (从索引1到数组末尾)
s2 := s[1:]              // 从切片创建,s2 = [3,4], len=2, cap=3

截取操作 [low:high] 遵循左闭右开原则,lowhigh 可省略(默认为 0 和 len)。

4. 切片的基本操作

4.1 访问元素

通过索引 s[i] 访问,索引范围 0 <= i < len(s),越界会导致 panic。

4.2 截取切片

复制代码
s := []int{0,1,2,3,4,5}
s1 := s[2:5]      // [2,3,4], len=3, cap=4 (从索引2到底层数组末尾)
s2 := s[:3]       // [0,1,2], len=3, cap=6
s3 := s[3:]       // [3,4,5], len=3, cap=3
s4 := s[:]        // 整个切片,len=6, cap=6

截取产生的新切片与原始切片共享底层数组,修改会影响对方。

4.3 追加元素(append

复制代码
s := []int{1,2,3}
s = append(s, 4)        // [1,2,3,4], len=4, cap 可能变化
s = append(s, 5,6)      // 追加多个
s = append(s, []int{7,8}...) // 追加另一个切片(需要展开)
  • append 在尾部追加元素,如果 len < cap,直接使用现有空间;否则触发扩容。

  • 必须将返回值赋值给原切片(因为可能扩容导致底层数组改变)。

4.4 删除元素

Go 没有内置删除函数,通常使用截取和追加组合实现。

  • 删除开头元素

    复制代码
    s = s[1:]  // 删除第一个
    s = s[n:]  // 删除前n个
  • 删除末尾元素

    复制代码
    s = s[:len(s)-1]      // 删除最后一个
    s = s[:len(s)-n]      // 删除后n个
  • 删除中间元素

    复制代码
    i := 2 // 要删除的索引
    s = append(s[:i], s[i+1:]...)
  • 清空切片(保留容量):

    复制代码
    s = s[:0]  // len=0, cap不变

4.5 拷贝切片(copy

复制代码
src := []int{1,2,3}
dst := make([]int, len(src))
n := copy(dst, src) // n = 3,dst 独立于 src
  • copy 返回实际复制的元素个数(取 min(len(dst), len(src)))。

  • dstsrc 可以重叠,复制正确。

  • 浅拷贝:只复制元素值,不复制元素内部的引用(但如果元素是指针,则共享指向的对象)。

4.6 遍历切片

复制代码
for i, v := range s {
    // i 是索引,v 是元素值的副本
}
for i := range s {
    // 只使用索引
}
for _, v := range s {
    // 只使用值
}

5. 切片的引用传递特性

切片本身是一个包含指针的小结构体(slice header),在函数间传递时,会复制 slice header(值传递),但复制的 header 中的 array 指针指向同一个底层数组。因此,对切片元素的修改会反映到原切片;但对切片长度、容量的修改(如 append 导致扩容)不会影响原切片,除非返回新切片并重新赋值。

复制代码
func modify(s []int) {
    s[0] = 100           // 修改元素,原切片受影响
    s = append(s, 200)   // 可能扩容,s 变成新 header,原切片不受影响
}

6. 切片的扩容机制

append 导致长度超过容量时,切片会扩容。扩容规则(Go 1.19+):

  1. 如果新容量 > 旧容量的两倍,直接使用新容量。

  2. 否则,如果旧容量 < 256,新容量 = 旧容量 * 2。

  3. 否则,循环计算新容量:newcap = oldcap + (oldcap + 3*256)/4,直到 newcap >= 所需容量

  4. 扩容后,内存分配会按 mallocgc 的对齐规则进行向上取整,可能导致最终容量略大于计算值(受内存分配类影响)。

示例:旧容量 512,追加 1 个元素后,计算新容量:

  • 所需容量 = 513,旧容量 512 ≥ 256,计算:

    • 512 + (512+768)/4 = 512 + 1280/4 = 512 + 320 = 832,满足 ≥513,新容量 = 832。
  • 内存对齐:元素大小 8 字节,832*8 = 6656 字节,实际分配的内存块可能为 6784 字节(对应 size class 49),因此最终容量 = 6784/8 = 848。

扩容过程

  • 分配新底层数组(可能更大)。

  • 将旧元素拷贝到新数组。

  • 返回新切片(array 指向新地址,len 增加,cap 更新)。

7. 切片的注意事项和常见陷阱

7.1 长度与容量的区别

  • len 是可访问元素的数量,cap 是已分配内存能容纳的最大元素数。

  • 访问 s[len]s[cap-1] 会导致 panic,即使内存已分配。

  • 截取操作可能产生容量很大的切片,隐藏了大量未使用的内存。

7.2 切片共享底层数组的副作用

截取和简单赋值产生的切片共享底层数组,修改一个会影响其他。当需要独立副本时,应使用 copy 或完整复制。

7.3 扩容导致地址变更

append 可能触发扩容,扩容后底层数组改变,旧切片与新切片不再共享内存。如果多个变量引用同一底层数组,扩容后需注意更新所有引用。

7.4 内存泄漏风险

截取大切片的一部分并持有,可能导致大数组无法被垃圾回收。例如:

复制代码
var all []byte // 一个大切片
part := all[10:20] // part 持有整个底层数组,即使只引用小部分

解决方案:使用 copy 将所需部分复制到新切片。

7.5 切片不是并发安全的

多个 goroutine 并发读写同一切片(包括 append)需要加锁或使用其他同步机制。

7.6 零值切片与空切片

  • var s []intnil 切片,没有底层数组,lencap 都是 0。

  • s := []int{}make([]int, 0) 是非 nil 的空切片,有底层数组(但长度为 0)。

  • 两者在 append 时行为一致,但在 JSON 序列化等场景有区别(nil 序列化为 null,空切片序列化为 [])。

7.7 append 导致容量变化的不确定性

由于扩容规则和内存对齐,不能依赖 cap 的精确值,只应关注是否满足需求。

8. 问题解答汇总

问题1

复制代码
s := make([]int, 10)  // len=10, cap=10
s = append(s, 10)     // len=11, cap=20 (原容量<256,翻倍)

答案[0 0 0 0 0 0 0 0 0 0 10], len=11, cap=20

问题2

复制代码
s := make([]int, 0, 10) // len=0, cap=10
s = append(s, 10)       // len=1, cap=10 (容量足够,不扩容)

答案[10], len=1, cap=10

问题3

复制代码
s := make([]int, 10, 11) // len=10, cap=11
s = append(s, 10)        // len=11, cap=11 (容量足够)

答案[0 0 0 0 0 0 0 0 0 0 10], len=11, cap=11

问题4

复制代码
s := make([]int, 10, 12) // len=10, cap=12
s1 := s[8:]              // 从索引8开始,len=2, cap=4

答案[0 0], len=2, cap=4

问题5

复制代码
s := make([]int, 10, 12)
s1 := s[8:9]             // len=1, cap=4 (容量仍从索引8到原数组末尾)

答案[0], len=1, cap=4

问题6

复制代码
s := make([]int, 10, 12)
s1 := s[8:]
s1[0] = -1                // 修改共享元素

答案s 变为 [0 0 0 0 0 0 0 0 -1 0]

问题7

复制代码
s := make([]int, 10, 12)
v := s[10]                // 索引10 ≥ len(10),越界

答案:发生 panic

问题8

复制代码
s := make([]int, 10, 12)
s1 := s[8:]
s1 = append(s1, []int{10,11,12}...) // s1 扩容,底层数组改变
v := s[10]                // s 仍为原数组,len=10,s[10] 越界

答案 :访问 s[10] 发生 panic

问题9

复制代码
s := make([]int, 10, 12)
s1 := s[8:]
changeSlice(s1)           // 修改 s1[0] = -1
func changeSlice(s1 []int) { s1[0] = -1 }

答案s 变为 [0 0 0 0 0 0 0 0 -1 0]

问题10

复制代码
s := make([]int, 10, 12)
s1 := s[8:]
changeSlice(s1)           // 内部 append,不影响外部 s1 的 len/cap
func changeSlice(s1 []int) { s1 = append(s1, 10) }

答案

  • s: [0 0 0 0 0 0 0 0 0 0], len=10, cap=12

  • s1: [0 0], len=2, cap=4 (外部 s1 未变)

问题11

复制代码
s := []int{0,1,2,3,4}
s = append(s[:2], s[3:]...) // 删除索引2的元素

答案s 变为 [0 1 3 4], len=4, cap=5,访问 s[4] 越界 panic

问题12

复制代码
s := make([]int, 512)      // len=512, cap=512
s = append(s, 1)           // 触发扩容

答案:len=513, cap=848 (扩容规则+内存对齐)

9. 总结

  • 切片是 Go 中灵活、高效的序列类型,理解其内部结构(slice header)是正确使用的基础。

  • 注意区分长度和容量,避免越界。

  • 共享底层数组的特性容易导致意外副作用,需谨慎处理截取和传递。

  • 扩容机制随 Go 版本变化,了解当前版本的规则有助于容量预估和性能优化。

  • 切片非并发安全,多 goroutine 操作需加锁。

  • 使用 copy 创建独立副本,避免内存泄漏。

掌握切片的底层原理和常见陷阱,能帮助开发者写出更健壮、高效的 Go 代码。

相关推荐
源代码•宸6 小时前
简版抖音项目——项目需求、项目整体设计、Gin 框架使用、视频模块方案设计、用户与鉴权模块方案设计、JWT
经验分享·后端·golang·音视频·gin·jwt·gorm
nix.gnehc7 小时前
深入浅出 Go 内存管理(二):预分配、GC 与内存复用实战
golang
creator_Li7 小时前
Golang的Channel
golang·channel
nix.gnehc9 小时前
深入理解Go并发核心:GMP模型与Goroutine底层原理
开发语言·算法·golang
nix.gnehc9 小时前
深入浅出 Go 内存管理(一):三级缓存、逃逸分析与内存碎片
golang
nix.gnehc10 小时前
Go进阶攻坚+专家深耕级学习清单|聚焦高并发、高性能中间件/底层框架开发(Java开发者专属)
学习·中间件·golang
普通网友1 天前
PL/SQL语言的正则表达式
开发语言·后端·golang
一个处女座的程序猿O(∩_∩)O1 天前
Go语言Map值不可寻址深度解析:原理、影响与解决方案
开发语言·后端·golang