Go: Go 数组与切片

切片(slice)是 Go 处理集合数据的核心——它是对数组的轻量"view",动态扩容让集合操作既快又灵活。

数组在 Go 中很少直接使用(因为长度固定),但切片无处不在。理解切片必须先理解数组——切片的底层就是一个指向数组的指针 + 长度 + 容量。这节课你将掌握 Go 集合类型的全部核心。

1. 你将学到


2. 一个数据采集工程师的真实故事

(1) 痛点:固定数组装不下动态数据

Alice 是一个数据采集工程师,她最近需要写一个爬虫:

"我要从 5 个数据源各抓 10,000 条记录,存到内存里做去重。最开始我用 Python 列表,'append' 一下就完事。换成 Go 之后,我用 [10000]int 这种定长数组,结果第一条记录就 panic:index out of range。"

她打开自己写的 Go 代码:

GO
// 第一版:定长数组,无法扩展
var records [10000]int
for i := 0; i < 50000; i++ {
    records[i] = i  // ❌ i >= 10000 就 panic!
}

老板说:"数据源数量每天都在变,你这个定长数组没法用。"Alice 这才意识到:Go 的数组是值类型,长度是类型的一部分,[5]int[10]int 是不同类型。

(2) Go 的解法:用切片(slice)

GO
// data_collector.go
package main

import "fmt"

func main() {
    // 切片:动态长度,自动扩容
    var records []int  // nil 切片,可以 append

    // 模拟 5 个数据源各抓 10,000 条
    for source := 1; source <= 5; source++ {
        for i := 0; i < 10000; i++ {
            value := source*10000 + i
            records = append(records, value)  // 自动扩容
        }
    }

    fmt.Printf("总记录数:%d\n", len(records))
    fmt.Printf("底层容量:%d\n", cap(records))
    fmt.Printf("前 3 条:%v\n", records[:3])
    fmt.Printf("后 3 条:%v\n", records[len(records)-3:])
}

输出:

TEXT 📖 仅展示
总记录数:50000
底层容量:65536
前 3 条:[10000 10001 10002]
后 3 条:[59997 59998 59999]

(3) 收益:array vs 切片

维度 array [n]T 切片 []T
长度 编译期固定 运行时可变
类型 [5]int[10]int []int 通用
传参 值传递(拷贝整个数组) 引用传递(仅 24 字节)
扩容 不支持 append 自动
使用场景 固定大小(如 buffer) 99% 场景
💡 提示: Go 中"集合"的 99% 场景都用切片,数组仅在固定大小(如 SHA-256 的 32 字节缓冲区)或需要值语义时使用。


3. array

(1) 数组的定义与初始化

GO
package main

import "fmt"

func main() {
    // 写法 1:声明 + 初始化
    var a1 [5]int = [5]int{1, 2, 3, 4, 5}

    // 写法 2:类型推断
    a2 := [5]int{1, 2, 3, 4, 5}

    // 写法 3:部分初始化(其余为零值)
    a3 := [5]int{1, 2}  // [1 2 0 0 0]

    // 写法 4:用 ... 让编译器推断长度
    a4 := [...]int{1, 2, 3, 4}  // 长度 4

    // 写法 5:指定索引初始化
    a5 := [5]int{0: 10, 4: 50}  // [10 0 0 0 50]

    fmt.Println(a1, a2, a3, a4, a5)
}

输出:

TEXT 📖 仅展示
[1 2 3 4 5] [1 2 3 4 5] [1 2 0 0 0] [1 2 3 4] [10 0 0 0 50]

(2) 数组遍历

GO
package main

import "fmt"

func main() {
    nums := [4]string{"Alice", "Bob", "Charlie", "Dave"}

    // 方式 1:for loop
    for i := 0; i < len(nums); i++ {
        fmt.Printf("%d: %s\n", i, nums[i])
    }

    // 方式 2:for range
    for i, name := range nums {
        fmt.Printf("[%d] %s\n", i, name)
    }
}

(3) 数组是值类型

GO
package main

import "fmt"

func modify(arr [3]int) {
    arr[0] = 999  // 修改的是副本
    fmt.Printf("函数内:%v\n", arr)
}

func main() {
    a := [3]int{1, 2, 3}
    modify(a)
    fmt.Printf("函数外:%v\n", a)  // 原数组不变
}

输出:

TEXT 📖 仅展示
函数内:[999 2 3]
函数外:[1 2 3]
🔥 易错: Go 数组是值类型,传参会复制整个数组。100 万个元素的数组传参 = 复制 100 万元素 = 性能灾难。用切片替代。

▶ 示例:数组比较(仅限同类型同长度)

GO
package main

import "fmt"

func main() {
    a := [3]int{1, 2, 3}
    b := [3]int{1, 2, 3}
    c := [3]int{1, 2, 4}

    fmt.Println(a == b)  // true
    fmt.Println(a == c)  // false

    // 不同长度或类型不可比较
    // d := [4]int{1, 2, 3, 4}
    // fmt.Println(a == d)  // ❌ mismatched types
}
▶ 试一试

4. 切片 4 种创建方式

(1) 切片 = 数组的"视图"

100%
graph TB
    subgraph Slice["切片 slice(24 字节)"]
        P["ptr<br/>指向底层数组"]
        L["len<br/>长度"]
        C["cap<br/>容量"]
    end
    subgraph Array["底层数组"]
        A0["[0]"]
        A1["[1]"]
        A2["[2]"]
        A3["[3]"]
        A4["[4]"]
        A5["[5]"]
    end
    P --> A1
    P -.-> A2
    P -.-> A3

切片是对底层数组的引用——只包含 24 字节(ptr + len + cap)。多个切片可以共享同一个底层数组。

(2) 创建方式 1:直接声明 nil 切片

GO
var s []int  // nil 切片,len=0, cap=0
// 可以 append,但不能用 s[0] 访问

(3) 创建方式 2:字面量

GO
s := []int{1, 2, 3, 4, 5}  // 创建并初始化
// 底层数组 = [1 2 3 4 5],len=cap=5

(4) 创建方式 3:make(指定 len 和 cap)

GO
// 语法:make([]T, len, cap)
s := make([]int, 5)       // len=5, cap=5, 全部为零值 [0 0 0 0 0]
s := make([]int, 3, 10)   // len=3, cap=10

▶ 示例:make 4 种用法对比

GO
package main

import "fmt"

func main() {
    // 方式 1:nil 切片
    var s1 []int
    fmt.Printf("s1: len=%d cap=%d nil=%v\n", len(s1), cap(s1), s1 == nil)

    // 方式 2:字面量
    s2 := []int{1, 2, 3}
    fmt.Printf("s2: len=%d cap=%d %v\n", len(s2), cap(s2), s2)

    // 方式 3:make 长度
    s3 := make([]int, 5)
    fmt.Printf("s3: len=%d cap=%d %v\n", len(s3), cap(s3), s3)

    // 方式 4:make 长度 + 容量
    s4 := make([]int, 3, 10)
    fmt.Printf("s4: len=%d cap=%d %v\n", len(s4), cap(s4), s4)

    // 方式 5:从数组切片
    arr := [5]int{10, 20, 30, 40, 50}
    s5 := arr[1:4]  // [20 30 40],len=3 cap=4(剩余 4 个元素)
    fmt.Printf("s5: len=%d cap=%d %v\n", len(s5), cap(s5), s5)
}
▶ 试一试

输出:

TEXT 📖 仅展示
s1: len=0 cap=0 nil=true
s2: len=3 cap=3 [1 2 3]
s3: len=5 cap=5 [0 0 0 0 0]
s4: len=3 cap=10 [0 0 0]
s5: len=3 cap=4 [20 30 40]

(5) 4 种创建方式速查

方式 语法 适用场景
nil 切片 var s []T 延迟初始化
字面量 []T{1, 2, 3} 已知全部元素
make make([]T, len, cap) 预分配容量
数组切片 arr[low:high] 子数组视图

5. len() 与 cap()

(1) len 和 cap 的关系

GO
package main

import "fmt"

func main() {
    s := make([]int, 3, 10)  // len=3, cap=10

    fmt.Printf("初始:len=%d cap=%d\n", len(s), cap(s))
    // len=3:可访问 s[0], s[1], s[2]
    // cap=10:底层数组有 10 个位置,还可 append 7 个
}

(2) cap 的意义

cap - len = 还能 append 多少元素不扩容。如果超过 cap,append 会分配新的底层数组(通常是 2 倍扩容)。

▶ 示例:len vs cap 的实际区别

GO
package main

import "fmt"

func main() {
    s := make([]int, 0, 5)  // len=0, cap=5

    for i := 1; i <= 8; i++ {
        s = append(s, i)
        fmt.Printf("append %d: len=%d cap=%d %v\n", i, len(s), cap(s), s)
    }
}
▶ 试一试

输出:

TEXT 📖 仅展示
append 1: len=1 cap=5 [1]
append 2: len=2 cap=5 [1 2]
append 3: len=3 cap=5 [1 2 3]
append 4: len=4 cap=5 [1 2 3 4]
append 5: len=5 cap=5 [1 2 3 4 5]
append 6: len=6 cap=10 [1 2 3 4 5 6]  ← cap 翻倍
append 7: len=7 cap=10 [1 2 3 4 5 6 7]
append 8: len=8 cap=10 [1 2 3 4 5 6 7 8]
💡 提示: Go 的扩容策略:当 len == cap 时,新 cap = 旧 cap * 2(小切片)或 旧 cap * 1.25(大切片,约 > 256)。


6. append 动态扩容

(1) append 基础

GO
package main

import "fmt"

func main() {
    var s []int  // nil

    s = append(s, 1)       // [1]
    s = append(s, 2, 3, 4) // [1 2 3 4]

    // 批量 append:把一个 slice 拆开
    s2 := []int{5, 6, 7}
    s = append(s, s2...)   // [1 2 3 4 5 6 7]

    fmt.Println(s)
}

输出:

TEXT 📖 仅展示
[1 2 3 4 5 6 7]

▶ 示例:append 删除元素(无内置 delete)

GO
package main

import "fmt"

// 删除索引 i 处的元素
func removeAt(s []int, i int) []int {
    // 拼接:s[:i] + s[i+1:]
    return append(s[:i], s[i+1:]...)
}

func main() {
    s := []int{1, 2, 3, 4, 5}
    s = removeAt(s, 2)  // 删除 3
    fmt.Println(s)       // [1 2 4 5]
}
▶ 试一试

输出:

TEXT 📖 仅展示
[1 2 4 5]

(2) append 的陷阱:可能修改原数组

GO
package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5}
    s2 := s[:3]  // [1 2 3],共享底层数组

    s2 = append(s2, 99)  // 没扩容,修改了 s 的底层数组!
    fmt.Println("s:", s)    // [1 2 3 99 5]
    fmt.Println("s2:", s2)  // [1 2 3 99]
}

输出:

TEXT 📖 仅展示
s: [1 2 3 99 5]
s2: [1 2 3 99]
🔥 易错: 这是 Go 最经典的坑——切片共享底层数组,append 可能影响其他切片。需要严格隔离时用 copy() 或强制扩容。


7. 切片截取 [low:high:max]

(1) 三种切片表达式

GO
package main

import "fmt"

func main() {
    arr := [5]int{10, 20, 30, 40, 50}

    // 形式 1:[low:high] —— 取 [low, high)
    s1 := arr[1:4]
    fmt.Printf("arr[1:4] = %v, len=%d cap=%d\n", s1, len(s1), cap(s1))

    // 形式 2:[low:] —— 从 low 到末尾
    s2 := arr[2:]
    fmt.Printf("arr[2:]  = %v, len=%d cap=%d\n", s2, len(s2), cap(s2))

    // 形式 3:[:high] —— 从头到 high
    s3 := arr[:3]
    fmt.Printf("arr[:3]  = %v, len=%d cap=%d\n", s3, len(s3), cap(s3))

    // 形式 4:[low:high:max] —— 限制 cap(防止意外追加影响原数组)
    s4 := arr[1:3:4]
    fmt.Printf("arr[1:3:4] = %v, len=%d cap=%d\n", s4, len(s4), cap(s4))
}

输出:

TEXT 📖 仅展示
arr[1:4] = [20 30 40], len=3 cap=4
arr[2:]  = [30 40 50], len=3 cap=3
arr[:3]  = [10 20 30], len=3 cap=5
arr[1:3:4] = [20 30], len=2 cap=3

(2) [low:high:max] 的用途

max 限制 cap,防止 append 覆盖到原数组的其他部分:

GO
package main

import "fmt"

func main() {
    arr := [5]int{10, 20, 30, 40, 50}

    // 不限制 max:cap=4,append 后会覆盖 arr[3]
    s1 := arr[1:3]  // [20 30], cap=4
    s1 = append(s1, 999)
    fmt.Println("arr:", arr)  // [10 20 30 999 50] ← 被覆盖!

    // 限制 max=3:cap=2,append 触发新数组
    s2 := arr[1:3:3]  // [20 30], cap=2
    s2 = append(s2, 888)
    fmt.Println("arr:", arr)  // [10 20 30 999 50] ← 不变
    fmt.Println("s2:", s2)    // [20 30 888]
}

输出:

TEXT 📖 仅展示
arr: [10 20 30 999 50]
arr: [10 20 30 999 50]
s2: [20 30 888]

8. 切片共享底层数组的坑(重点)

(1) 坑 1:修改切片元素影响所有切片

GO
package main

import "fmt"

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    s1 := arr[0:3]  // [1 2 3]
    s2 := arr[2:5]  // [3 4 5]

    s1[2] = 999  // 同一个底层数组!
    fmt.Println("s1:", s1)  // [1 2 999]
    fmt.Println("s2:", s2)  // [999 4 5]
    fmt.Println("arr:", arr) // [1 2 999 4 5]
}

(2) 坑 2:for range 时 append 导致死循环

GO
package main

import "fmt"

func main() {
    s := []int{1, 2, 3}
    for _, v := range s {
        s = append(s, v*10)  // 危险:s 在变长
    }
    fmt.Println(s)  // 输出不确定
}
🔥 易错: for range 期间永远不要修改切片本身。Go 1.22 之前,for i, v := range s 中的 s 是循环开始时的长度快照;Go 1.22 修复了部分场景,但仍要小心。

(3) 坑 3:切片作为函数参数被意外修改

GO
package main

import "fmt"

// 切片是引用传递,函数内的修改会影响外部
func addElement(s []int) {
    s = append(s, 100)  // 可能修改原数组(如果没扩容)
    fmt.Printf("函数内: %v\n", s)
}

func main() {
    s := make([]int, 3, 10)
    s[0], s[1], s[2] = 1, 2, 3

    addElement(s)
    fmt.Printf("函数外: %v\n", s)  // 取决于是否扩容
}

(4) 用 copy() 解决共享问题

GO
package main

import "fmt"

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

    // copy 出一个独立副本
    s2 := make([]int, len(s1))
    copy(s2, s1)

    s1[0] = 999
    fmt.Println("s1:", s1)  // [999 2 3]
    fmt.Println("s2:", s2)  // [1 2 3] ← 不受影响
}

▶ 示例:copy + append 实现切片插入

GO
package main

import "fmt"

// 在索引 i 处插入元素 val
func insert(s []int, i int, val int) []int {
    // 1. 扩容 1 个位置
    s = append(s, 0)

    // 2. 把 i 及之后的元素后移一位
    copy(s[i+1:], s[i:])

    // 3. 写入新元素
    s[i] = val

    return s
}

func main() {
    s := []int{1, 2, 4, 5}
    s = insert(s, 2, 3)
    fmt.Println(s)  // [1 2 3 4 5]
}
▶ 试一试

输出:

TEXT 📖 仅展示
[1 2 3 4 5]

9. 完整示例:动态数据采集器

把切片的所有特性串起来,构建一个多源数据采集 + 去重 + 过滤的工具:

GO
// collector.go
package main

import (
    "fmt"
    "strings"
)

// 数据源模拟函数
func fetchFromSource(sourceName string) []string {
    data := map[string][]string{
        "GitHub": {"repo:go", "repo:docker", "issue:bug", "repo:go"},  // 最后一个是重复
        "NPM":    {"pkg:react", "pkg:vue", "pkg:react"},              // 重复
        "PyPI":   {"pkg:requests", "pkg:flask"},
    }
    return data[sourceName]
}

// 去重:用 map 做哈希集合
func deduplicate(data []string) []string {
    seen := make(map[string]bool)
    result := make([]string, 0, len(data))

    for _, item := range data {
        if !seen[item] {
            seen[item] = true
            result = append(result, item)
        }
    }
    return result
}

// 过滤:只保留 repo: 开头的
func filterRepos(data []string) []string {
    result := make([]string, 0)
    for _, item := range data {
        if strings.HasPrefix(item, "repo:") {
            result = append(result, item)
        }
    }
    return result
}

// Pipeline:组合多个步骤
func collectPipeline() (total, unique, repos int, sources []string) {
    defer func() {
        // 命名返回值 + defer 收集统计
        fmt.Printf("\n[采集完成] total=%d unique=%d repos=%d sources=%v\n",
            total, unique, repos, sources)
    }()

    allSources := []string{"GitHub", "NPM", "PyPI"}
    var allData []string

    for _, src := range allSources {
        sources = append(sources, src)
        data := fetchFromSource(src)
        allData = append(allData, data...)
        total += len(data)
    }

    uniqueData := deduplicate(allData)
    unique = len(uniqueData)

    repoData := filterRepos(uniqueData)
    repos = len(repoData)

    fmt.Println("\n=== 去重后的数据 ===")
    for _, item := range uniqueData {
        fmt.Printf("  %s\n", item)
    }

    fmt.Println("\n=== 过滤 repo 的数据 ===")
    for _, item := range repoData {
        fmt.Printf("  %s\n", item)
    }

    return total, unique, repos, sources
}

func main() {
    collectPipeline()
}

预期输出:

TEXT 📖 仅展示
=== 去重后的数据 ===
  repo:go
  repo:docker
  issue:bug
  pkg:react
  pkg:vue
  pkg:requests
  pkg:flask

=== 过滤 repo 的数据 ===
  repo:go
  repo:docker

[采集完成] total=9 unique=7 repos=2 sources=[GitHub NPM PyPI]
🔥 易错:make([]string, 0, len(data)) 预分配容量,避免 append 频繁扩容。这是性能优化的关键技巧。


❓ 常见问题

Q 数组和切片底层是什么关系?
A 切片的底层就是一个数组。切片包含 24 字节:8 字节指针(指向数组某元素)+ 8 字节长度 + 8 字节容量。多个切片可以共享同一个底层数组。
Q append 一定会扩容吗?
A 不一定。只有当 len == cap 时才会扩容(分配新数组,2 倍复制)。如果还有容量空间,直接写入原数组。
Q 切片作为函数参数是值传递还是引用传递?
A 切片本身(24 字节)是值传递,但切片指向的底层数组是共享的。所以函数内修改元素会影响外部,但 append 是否影响外部取决于是否扩容。
Q [low:high:max] 三参数的 max 什么时候必须用?
A 当你想限制切片的容量,防止 append 影响原数组的其他元素时使用。常见场景:从大切片中取子切片 + 后续 append。
Q nil 切片和空切片有什么区别?
A nil 切片(如 var s []int)== nil 为 true,len=0 cap=0;空切片(如 []int{}make([]int, 0))== nil 为 false,len=0 cap=0。两者都能 append,但 JSON 序列化结果不同:nil → null,空 → []
Q 切片删除元素有内置方法吗?
A 没有。Go 不像 Java 有 list.remove(i)。Go 用 append(s[:i], s[i+1:]...) 实现删除(但不保留顺序)。如果要保留顺序,需要手动移动元素。
Q 怎么复制切片?
A 3 种方式:(1) copy(dst, src) — 复制 min(len(dst), len(src)) 个元素;(2) append([]T(nil), src...) — 复制全部;(3) 手动 for range。
Q 切片和数组在 range 时性能差异?
A 数组 range 会复制整个数组(性能差);切片 range 只复制 24 字节(性能好)。这就是为什么切片是"集合"的事实标准。

📖 小节


📝 作业

  1. 基础题(难度⭐):用切片实现一个 reverse(s []int) []int 函数,把切片元素反转。提示:用 for 循环 + 临时变量,或原地交换。要求测试 reverse([]int{1,2,3,4,5})[5 4 3 2 1]

  2. 进阶题(难度⭐⭐):实现一个 uniqueSorted(nums []int) []int 函数:去重 + 升序排序。提示:先 sort.Ints 排序,再遍历去重。要求处理 []int{3, 1, 4, 1, 5, 9, 2, 6, 5} 输出 [1 2 3 4 5 6 9]

  3. 挑战题(难度⭐⭐⭐):实现一个滑动窗口最大值算法maxSlidingWindow(nums []int, k int) []int。例如 nums=[1,3,-1,-3,5,3,6,7], k=3 → 输出 [3,3,5,5,6,7]。要求:(1) 时间复杂度 O(n);(2) 用 deque(可用 []int 模拟)维护窗口;(3) 解释为什么直接遍历会超时(O(n*k))。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏