6.1 Go 切片学习笔记

6.1 Go 切片学习笔记

1. 切片基础 --- 创建与共享底层数组

切片是 Go 中最核心的数据结构,它是动态数组的"视图",由指针、长度、容量三部分组成:

go 复制代码
package main

import "fmt"

func main() {
    // 从数组创建切片:arr[low:high],包含 low 到 high-1 的元素
    arr := [5]int{1, 2, 3, 4, 5}
    slice1 := arr[1:4] // [2, 3, 4]

    // make() 创建切片:make([]type, length, capacity)
    slice2 := make([]string, 3, 5) // 长度3,容量5,元素为string零值""
    // 长度3:已有3个""元素可访问;容量5:底层数组可容纳5个元素

    // 切片字面量:直接声明(与数组字面量不同,没有长度)
    slice3 := []int{10, 20, 30} // 切片,不是数组!没有 [N]

    // nil 切片:var 声明未初始化
    var slice4 []int // slice4 == nil,len=0, cap=0

    if slice4 == nil {
        fmt.Println("slice4 is nil")
    }

    // len() 和 cap() 获取长度和容量
    length := len(slice2)
    capacity := cap(slice2)

    // 修改切片元素会影响原数组(共享底层内存)
    slice1[1] = 99 // arr[2] 也变成 99

    fmt.Println("Original array:", arr)
    fmt.Println("slice1:", slice1)
    fmt.Println("slice2:", slice2)
    fmt.Println("slice3:", slice3)
    fmt.Printf("slice2 - length: %d, capacity: %d\n", length, capacity)
}

执行结果:

复制代码
slice4 is nil
Original array: [1 2 99 4 5]
slice1: [2 99 4]
slice2: [  ]
slice3: [10 20 30]
slice2 - length: 3, capacity: 5

要点

  • 切片 ≠ 数组:数组有固定长度 [5]int,切片没有固定长度 []int
  • arr[1:4] --- 从数组创建切片,包含索引 1 到 3 的元素(不含索引4)
  • slice1[1] = 99 修改了切片元素,原数组 arr[2] 也变成 99 --- 切片和数组共享底层内存
  • make([]string, 3, 5) --- 创建长度3、容量5的切片,前3个元素为零值 "",还有2个预留空间
  • []int{10, 20, 30} --- 切片字面量(没有长度数字),Go 自动创建底层数组
  • var slice4 []int --- nil 切片,可以 == nil 判断(与空切片 []int{} 不同,空切片 != nil)
  • slice2 输出 [ ] --- 3个空字符串(string 的零值是 "")

2. append --- 追加元素与容量增长

append() 向切片追加元素,超过容量时自动扩容:

go 复制代码
package main

import "fmt"

func main() {
    // 从 nil 切片开始 append(完全合法)
    var numbers []int
    numbers = append(numbers, 1)       // 追加单个元素
    numbers = append(numbers, 2, 3, 4) // 追加多个元素

    // 用 ... 展开切片追加
    moreNumbers := []int{5, 6, 7}
    numbers = append(numbers, moreNumbers...) // 展开追加

    // 观察容量增长:make([]string, 0, 2)
    fruits := make([]string, 0, 2) // 长度0,容量2
    fmt.Printf("Initial - len: %d, cap: %d\n", len(fruits), cap(fruits))

    fruits = append(fruits, "apple")
    fmt.Printf("After 1 append - len: %d, cap: %d\n", len(fruits), cap(fruits))

    fruits = append(fruits, "banana")
    fmt.Printf("After 2 appends - len: %d, cap: %d\n", len(fruits), cap(fruits))

    // 第3次 append 超出容量,触发扩容:2 → 4
    fruits = append(fruits, "cherry")
    fmt.Printf("After 3 appends - len: %d, cap: %d\n", len(fruits), cap(fruits))

    // nil 切片可以直接 append
    var colors []string // nil 切片
    colors = append(colors, "red") // append 自动分配底层数组

    fmt.Println("Numbers:", numbers)
    fmt.Println("Fruits:", fruits)
    fmt.Println("Colors:", colors)
}

执行结果:

复制代码
Initial - len: 0, cap: 2
After 1 append - len: 1, cap: 2
After 2 appends - len: 2, cap: 2
After 3 appends - len: 3, cap: 4
Numbers: [1 2 3 4 5 6 7]
Fruits: [apple banana cherry]
Colors: [red]

要点

  • append(slice, element) --- 返回新切片(长度+1),必须用 = 接住返回值

    go 复制代码
    numbers = append(numbers, 1) // 必须赋值!append 可能返回新底层数组
  • append(numbers, 2, 3, 4) --- 同时追加多个元素

  • append(numbers, moreNumbers...) --- 用 ... 展开切片追加

  • 容量增长过程:

    • Initial: len=0, cap=2(预留2个位置)
    • append "apple": len=1, cap=2(还剩1个位置)
    • append "banana": len=2, cap=2(满了)
    • append "cherry": len=3, cap=4(超出容量,扩容到4)--- 容量翻倍!
  • nil 切片可以 append:Go 自动分配底层数组,无需 make 初始化

  • 扩容策略:通常翻倍(2→4→8→16...),但 Go 运行时可能根据情况调整


3. 浅拷贝与深拷贝 --- copy()

切片的"拷贝"有浅拷贝(共享底层数组)和深拷贝(独立底层数组)两种:

go 复制代码
package main

import "fmt"

func main() {
    original := []int{1, 2, 3, 4, 5}

    // 浅拷贝:original[:] 创建新切片头,但共享底层数组
    shallow := original[:]
    shallow[0] = 999 // 修改 shallow 也修改 original!
    fmt.Println("After modifying shallow copy:")
    fmt.Println("Original:", original) // [999 2 3 4 5] --- 被修改了
    fmt.Println("Shallow:", shallow)    // [999 2 3 4 5]

    // 重置 original
    original[0] = 1

    // 深拷贝:copy() 创建独立底层数组
    deep := make([]int, len(original)) // 先创建目标切片
    copy(deep, original)               // copy(dest, src) --- 复制元素
    deep[0] = 888                      // 修改 deep 不影响 original
    fmt.Println("\nAfter modifying deep copy:")
    fmt.Println("Original:", original) // [1 2 3 4 5] --- 未被修改
    fmt.Println("Deep:", deep)         // [888 2 3 4 5]

    // copy 返回复制的元素数量
    source := []string{"a", "b", "c", "d", "e"}
    destination := make([]string, 3)   // 目标长度3
    copied := copy(destination, source) // 只能复制3个(目标容量限制)
    fmt.Printf("\nCopied %d elements: %v\n", copied, destination)

    // 小切片复制到大切片
    small := []int{100, 200}
    large := make([]int, 5)
    n := copy(large, small) // 只复制2个,剩余3个保持零值0
    fmt.Printf("Copied %d elements to large slice: %v\n", n, large)
}

执行结果:

复制代码
After modifying shallow copy:
Original: [999 2 3 4 5]
Shallow: [999 2 3 4 5]

After modifying deep copy:
Original: [1 2 3 4 5]
Deep: [888 2 3 4 5]

Copied 3 elements: [a b c]
Copied 2 elements to large slice: [100 200 0 0 0]

要点

  • 浅拷贝 original[:] --- 新切片头 + 共享底层数组,修改互相影响
  • 深拷贝 copy(dest, src) --- 创建独立底层数组,修改互不影响
    • 步骤:先 make([]int, len) 创建目标 → 再 copy(dest, src) 复制元素
  • copy(dest, src) 返回实际复制的元素数 = min(len(dest), len(src))
    • 目标长度3,源长度5:只复制3个 → [a b c]
    • 源长度2,目标长度5:复制2个,剩余保持零值 → [100 200 0 0 0]
  • copy 不会扩容目标切片!目标切片必须预先 make 并有足够长度

4. 切片容量与底层共享 --- 深入理解

切片容量是从切片起始位置到底层数组末尾的元素数量,理解容量是理解切片的关键:

go 复制代码
package main

import "fmt"

func main() {
    // make 指定长度和容量
    slice1 := make([]int, 5, 10) // 5个零值元素,底层数组容量10
    fmt.Printf("slice1 - len: %d, cap: %d, slice: %v\n", len(slice1), cap(slice1), slice1)

    for i := range slice1 {
        slice1[i] = i * 2
    }
    fmt.Printf("After filling: %v\n", slice1)

    // 从数组切片:容量 = 底层数组从起始位置到末尾的长度
    array := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    slice2 := array[2:6] // 索引2到5,长度4
    // 容量 = 10 - 2 = 8(从索引2到数组末尾)
    fmt.Printf("slice2 - len: %d, cap: %d, slice: %v\n", len(slice2), cap(slice2), slice2)

    // 修改切片会影响原数组
    slice2[0] = 999 // array[2] 也变成 999
    fmt.Printf("Array after modifying slice2: %v\n", array)
    fmt.Printf("slice2 after modification: %v\n", slice2)

    // append 超出容量时触发扩容
    smallSlice := make([]int, 2, 3) // 长度2,容量3
    fmt.Printf("Initial smallSlice - len: %d, cap: %d\n", len(smallSlice), cap(smallSlice))

    smallSlice[0] = 10
    smallSlice[1] = 20

    fmt.Println("Appending elements and observing capacity:")
    // 第1次 append:还在容量内
    smallSlice = append(smallSlice, 30)
    fmt.Printf("After append(30) - len: %d, cap: %d, slice: %v\n", len(smallSlice), cap(smallSlice), smallSlice)

    // 第2次 append:超出容量,扩容 3→6
    smallSlice = append(smallSlice, 40)
    fmt.Printf("After append(40) - len: %d, cap: %d, slice: %v\n", len(smallSlice), cap(smallSlice), smallSlice)

    // 第3次 append 多个元素:7个 > 容量6,扩容 6→12
    smallSlice = append(smallSlice, 50, 60, 70)
    fmt.Printf("After append(50,60,70) - len: %d, cap: %d, slice: %v\n", len(smallSlice), cap(smallSlice), smallSlice)
}

执行结果:

复制代码
slice1 - len: 5, cap: 10, slice: [0 0 0 0 0]
After filling: [0 2 4 6 8]
slice2 - len: 4, cap: 8, slice: [3 4 5 6]
Array after modifying slice2: [1 2 999 4 5 6 7 8 9 10]
slice2 after modification: [999 4 5 6]
Initial smallSlice - len: 2, cap: 3
Appending elements and observing capacity:
After append(30) - len: 3, cap: 3, slice: [10 20 30]
After append(40) - len: 4, cap: 6, slice: [10 20 30 40]
After append(50,60,70) - len: 7, cap: 12, slice: [10 20 30 40 50 60 70]

要点

  • make([]int, 5, 10) --- 长度5(可访问5个元素),容量10(底层数组有10个位置)
  • array[2:6] 的容量 = 10-2 = 8 --- 从索引2到数组末尾(索引0-9),还有8个位置可用
  • slice2[0] = 999array[2] = 999 --- 切片与数组共享内存
  • append 扩容过程:
    • append(30):len 2→3, cap 3(刚好满了)
    • append(40):len 3→4, cap 3→6(超出容量,分配新底层数组,容量翻倍)
    • append(50,60,70):len 4→7, cap 6→12(再次超出,容量翻倍)
  • 关键:扩容时分配新底层数组,切片不再与原数组共享内存!

5. 切片重叠与完整切片表达式 [low:high:max]

多个切片可以共享同一底层数组,修改重叠区域会互相影响;完整切片表达式可以限制容量:

go 复制代码
package main

import "fmt"

func main() {
    base := []int{1, 2, 3, 4, 5, 6, 7, 8}
    slice3 := base[1:4] // [2, 3, 4],容量 = 8-1 = 7
    slice4 := base[3:6] // [4, 5, 6],容量 = 8-3 = 5

    fmt.Printf("base: %v\n", base)
    fmt.Printf("slice3 [1:4]: %v (len: %d, cap: %d)\n", slice3, len(slice3), cap(slice3))
    fmt.Printf("slice4 [3:6]: %v (len: %d, cap: %d)\n", slice4, len(slice4), cap(slice4))

    // slice3[2] 对应 base[3],slice4[0] 也对应 base[3] --- 重叠!
    slice3[2] = 999 // base[3] 变成 999,slice4[0] 也变成 999
    fmt.Printf("After modifying slice3[2] to 999:\n")
    fmt.Printf("base: %v\n", base)    // [1 2 3 999 5 6 7 8]
    fmt.Printf("slice3: %v\n", slice3) // [2 3 999]
    fmt.Printf("slice4: %v\n", slice4) // [999 5 6] --- 被影响了!

    // 完整切片表达式:[low:high:max],max 限制容量
    fullSlice := base[1:4:5] // len = 4-1 = 3, cap = 5-1 = 4
    fmt.Printf("Full slice expression [1:4:5] - len: %d, cap: %d, slice: %v\n",
        len(fullSlice), cap(fullSlice), fullSlice)
}

执行结果:

复制代码
base: [1 2 3 4 5 6 7 8]
slice3 [1:4]: [2 3 4] (len: 3, cap: 7)
slice4 [3:6]: [4 5 6] (len: 3, cap: 5)
After modifying slice3[2] to 999:
base: [1 2 3 999 5 6 7 8]
slice3: [2 3 999]
slice4: [999 5 6]
Full slice expression [1:4:5] - len: 3, cap: 4, slice: [2 3 999]

要点

  • base[1:4] 容量 = 7(从 base 索引1到末尾,8-1=7)
  • base[3:6] 容量 = 5(从 base 索引3到末尾,8-3=5)
  • slice32 和 slice40 都指向 base3 --- 重叠区域,修改一个另一个也变
  • base[1:4:5] --- 完整切片表达式(3索引),max=5 限制容量 = 5-1 = 4
    • 没有第三个索引时,容量到底层数组末尾(7)
    • 有第三个索引时,容量被限制为 max-low(4),更安全
    • 好处:append 不会侵入其他切片的区域,超出限制容量时会分配新底层数组

6. 扩容策略观察 --- 容量增长规律

观察 append 时容量的实际增长:

go 复制代码
package main

import "fmt"

func main() {
    fmt.Println("Demonstrating memory reallocation:")
    growSlice := []int{1} // len=1, cap=1
    for i := 0; i < 5; i++ {
        fmt.Printf("Before append - len: %d, cap: %d\n", len(growSlice), cap(growSlice))
        growSlice = append(growSlice, i)
        fmt.Printf("After append  - len: %d, cap: %d\n", len(growSlice), cap(growSlice))
        fmt.Println()
    }
}

执行结果:

复制代码
Demonstrating memory reallocation:
Before append - len: 1, cap: 1
After append  - len: 2, cap: 2

Before append - len: 2, cap: 2
After append  - len: 3, cap: 4

Before append - len: 3, cap: 4
After append  - len: 4, cap: 4

Before append - len: 4, cap: 4
After append  - len: 5, cap: 8

Before append - len: 5, cap: 8
After append  - len: 6, cap: 8

要点

  • 扩容规律:1 → 2 → 4 → 8 → ...(大致翻倍)
  • 容量4时 append 1个元素:len 从3→4,不需要扩容(还在容量内)
  • 容量4时再 append:len 从4→5,超出容量,扩容到8
  • 详细过程:
    • cap=1, append: len 1→2, cap 1→2(翻倍)
    • cap=2, append: len 2→3, cap 2→4(翻倍)
    • cap=4, append: len 3→4, cap 不变(容量够用)
    • cap=4, append: len 4→5, cap 4→8(翻倍)
    • cap=8, append: len 5→6, cap 不变(容量够用)
  • 扩容时 Go 分配新底层数组,拷贝旧数据到新数组,旧数组由 GC 回收
  • 性能提示:如果知道大致需要多少元素,用 make([]T, 0, n) 预分配容量,避免反复扩容

知识点总结

知识点 关键概念
切片创建 数组切片 arr[1:4]make([]T, len, cap)、字面量 []int{...}、nil 切片
共享底层数组 切片修改会影响原数组,这是切片的核心特性(也是常见 bug 来源)
append 追加 返回新切片,必须接住;超出容量自动扩容;nil 切片可以 append
浅拷贝 vs 深拷贝 [:] 共享底层 vs copy() 独立底层
copy 函数 copy(dest, src) 返回复制数量 = min(len(dest), len(src))
容量计算 arr[low:high] 的 cap = len(array) - low
重叠切片 同底层数组的多个切片有重叠区域,修改互相影响
完整切片表达式 [low:high:max] 限制容量,防止 append 侵入其他切片
扩容策略 大致翻倍(1→2→4→8...),预分配容量可避免频繁扩容
相关推荐
x861 小时前
Go 1.27 正式发布:泛型方法、encoding/json/v2、后量子签名 ML-DSA 与 SIMD
开发语言·golang
圣殿骑士-Khtangc2 小时前
Go-defer机制深度解析执行顺序与性能优化实战
golang
WongKyunban2 小时前
Go语言的简洁并发编程
开发语言·后端·golang
敢敢のwings14 小时前
智元 GO-2 与 AgiBot-World 深度解读
开发语言·后端·golang
FfHUCisI14 小时前
Golang 数据库连接池深度调优
android·数据库·golang
程序员小八77715 小时前
Java 快速转 Go
java·python·golang
名字还没想好☜18 小时前
Go 用 slices/maps 标准库泛型函数:告别手写 Contains、Sort、去重(Go 1.21)
开发语言·后端·算法·golang·go
ylj_dev21 小时前
从 0 构建 AI Workload Platform(五):Agent Runtime、工具权限与自然语言工作流
golang·工作流·ai agent·大模型应用开发·agent runtime
灯澜忆梦1 天前
【基于GO的Web开发9】gin框架返回json
前端·后端·golang·gin