数组与切片
数组与切片
想象你去超市买了一整箱牛奶——数组就像这个固定容量的箱子,装满了就不能再多放;而切片则像一个可以不断扩展的购物袋,东西多了就换个更大的袋子。在Go语言中,数组(Array)长度固定、是值类型;切片(Slice)长度可变、是引用类型。实际开发中,切片使用得远比数组多,但理解数组是理解切片的基础。
1. 核心概念
| 概念 | 说明 |
|---|---|
数组 [N]T |
长度固定,声明时必须指定大小;是值类型,赋值和传参会复制整个数组 |
切片 []T |
长度可变,底层引用一个数组;是引用类型,赋值和传参共享底层数组 |
make([]T, len, cap) |
创建切片的推荐方式,可指定长度和容量 |
append(slice, elems...) |
向切片追加元素,返回新切片;可能触发底层数组扩容 |
copy(dst, src) |
将 src 切片的内容复制到 dst,返回复制的元素个数 |
长度 len() |
切片中实际包含的元素个数 |
容量 cap() |
切片底层数组从切片起始位置到末尾的元素个数 |
2. 基本语法/用法
数组声明与初始化
// 声明一个长度为5的int数组,零值初始化
var arr1 [5]int
// 声明并初始化
var arr2 = [5]int{1, 2, 3, 4, 5}
// 让编译器自动推断长度
arr3 := [...]int{10, 20, 30}
// 指定索引初始化
arr4 := [5]int{1: 100, 3: 300} // [0, 100, 0, 300, 0]
切片声明与初始化
// 声明一个nil切片(零值为nil,但可以直接append)
var s1 []int
// 字面量初始化
s2 := []int{1, 2, 3}
// 从数组创建切片
arr := [5]int{10, 20, 30, 40, 50}
s3 := arr[1:4] // [20, 30, 40]
// 使用make创建切片
s4 := make([]int, 5) // 长度5,容量5
s5 := make([]int, 3, 10) // 长度3,容量10
nil切片和空切片([]int{}或make([]int, 0))在功能上是等价的——len、cap都为0,append都能正常使用。区别在于nil切片的JSON序列化结果是null,空切片则是[]。
切片操作
s := []int{10, 20, 30, 40, 50}
// 截取子切片 [左闭右开)
s1 := s[1:3] // [20, 30]
s2 := s[:3] // [10, 20, 30],从头开始
s3 := s[2:] // [30, 40, 50],到末尾
s4 := s[:] // [10, 20, 30, 40, 50],完整切片
// 长度与容量
fmt.Println(len(s)) // 5
fmt.Println(cap(s)) // 5
// append追加元素
s = append(s, 60) // 追加单个元素
s = append(s, 70, 80, 90) // 追加多个元素
s = append(s, []int{100}...) // 追加另一个切片
// copy复制切片
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src) // dst = [1, 2, 3], n = 3
append返回的是新切片,必须接收返回值。当len(s) == cap(s)时,append会分配新的底层数组,此时新旧切片不再共享数据。
s[low:high]截取时,新切片与原切片共享底层数组。修改子切片的元素会影响原切片。如需独立副本,请使用copy。
3. 示例代码
示例 1:基础用法(难度⭐)
package main
import "fmt"
func main() {
// ========== 数组 ==========
// 声明并初始化一个包含5个成绩的数组
scores := [5]int{90, 85, 78, 92, 88}
fmt.Println("成绩数组:", scores)
// 通过索引访问和修改元素
fmt.Println("第一个成绩:", scores[0])
scores[2] = 80 // 将第三个成绩改为80
fmt.Println("修改后:", scores)
// 数组遍历:传统for循环
fmt.Println("\n--- 传统for遍历 ---")
for i := 0; i < len(scores); i++ {
fmt.Printf("第%d个成绩: %d\n", i+1, scores[i])
}
// 数组遍历:range遍历
fmt.Println("\n--- range遍历 ---")
for index, value := range scores {
fmt.Printf("索引%d: %d\n", index, value)
}
// ========== 切片 ==========
// 从数组派生切片
top3 := scores[0:3] // 前3个成绩
fmt.Println("\n前3个成绩:", top3)
// 字面量创建切片
fruits := []string{"苹果", "香蕉", "橙子"}
fmt.Println("水果切片:", fruits)
// 使用append添加元素
fruits = append(fruits, "葡萄")
fmt.Println("添加葡萄后:", fruits)
// 长度与容量
fmt.Printf("长度: %d, 容量: %d\n", len(fruits), cap(fruits))
}
运行结果:
成绩数组: [90 85 78 92 88]
第一个成绩: 90
修改后: [90 85 80 92 88]
--- 传统for遍历 ---
第1个成绩: 90
第2个成绩: 85
第3个成绩: 80
第4个成绩: 92
第5个成绩: 88
--- range遍历 ---
索引0: 90
索引1: 85
索引2: 80
索引3: 92
索引4: 88
前3个成绩: [90 85 80]
水果切片: [苹果 香蕉 橙子]
添加葡萄后: [苹果 香蕉 橙子 葡萄]
长度: 4, 容量: 4
示例 2:进阶用法(难度⭐⭐)
package main
import "fmt"
func main() {
// ========== append与扩容 ==========
// 创建一个容量为3的切片
s := make([]int, 0, 3)
fmt.Printf("初始: len=%d, cap=%d, %v\n", len(s), cap(s), s)
// 逐个追加,观察容量变化
for i := 1; i <= 5; i++ {
s = append(s, i)
fmt.Printf("添加%d后: len=%d, cap=%d, %v\n", i, len(s), cap(s), s)
}
// ========== 切片共享底层数组 ==========
original := []int{10, 20, 30, 40, 50}
sub := original[1:3] // [20, 30]
fmt.Println("\n--- 共享底层数组演示 ---")
fmt.Println("原始切片:", original)
fmt.Println("子切片: ", sub)
// 修改子切片会影响原始切片
sub[0] = 999
fmt.Println("\n修改子切片后:")
fmt.Println("原始切片:", original) // original[1]也被改了
fmt.Println("子切片: ", sub)
// ========== 使用copy创建独立副本 ==========
fmt.Println("\n--- 使用copy独立复制 ---")
original = []int{10, 20, 30, 40, 50}
// 先截取子切片,再用copy复制一份独立的
subSlice := original[1:4] // [20, 30, 40]
independent := make([]int, len(subSlice))
copy(independent, subSlice)
independent[0] = 888
fmt.Println("原始切片:", original) // 不受影响
fmt.Println("独立副本:", independent) // 只有副本变了
// ========== 合并两个切片 ==========
fmt.Println("\n--- 合并切片 ---")
a := []int{1, 2, 3}
b := []int{4, 5, 6}
merged := append(a, b...)
fmt.Println("合并结果:", merged)
}
运行结果:
初始: len=0, cap=3, []
添加1后: len=1, cap=3, [1]
添加2后: len=2, cap=3, [1 2]
添加3后: len=3, cap=3, [1 2 3]
添加4后: len=4, cap=6, [1 2 3 4]
添加5后: len=5, cap=6, [1 2 3 4 5]
--- 共享底层数组演示 ---
原始切片: [10 20 30 40 50]
子切片: [20 30]
修改子切片后:
原始切片: [10 999 30 40 50]
子切片: [999 30]
--- 使用copy独立复制 ---
原始切片: [10 20 30 40 50]
独立副本: [888 30 40]
--- 合并切片 ---
合并结果: [1 2 3 4 5 6]
示例 3:综合应用(难度⭐⭐⭐)
package main
import "fmt"
// removeElement 从切片中删除指定索引的元素(保持顺序)
func removeElement(s []int, index int) []int {
if index < 0 || index >= len(s) {
return s // 索引越界,返回原切片
}
// 用append将index前后的部分拼接
return append(s[:index], s[index+1:]...)
}
// insertElement 在指定索引处插入元素
func insertElement(s []int, index int, value int) []int {
if index < 0 || index > len(s) {
return s
}
// 先扩展切片,再将元素后移,最后赋值
s = append(s, 0) // 追加一个占位元素
copy(s[index+1:], s[index:]) // 将index及之后的元素后移一位
s[index] = value // 在目标位置赋值
return s
}
// filterEven 过滤出偶数,返回新切片
func filterEven(s []int) []int {
result := make([]int, 0, len(s)/2) // 预估容量为一半
for _, v := range s {
if v%2 == 0 {
result = append(result, v)
}
}
return result
}
// 测试切片的内存共享与扩容机制
func sliceInternals() {
fmt.Println("=== 切片底层原理演示 ===")
// 创建一个底层数组
data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 截取子切片(共享底层数组)
s1 := data[2:5] // [2, 3, 4], len=3, cap=8
s2 := s1[1:3] // [3, 4], len=2, cap=7
fmt.Printf("data: %v, len=%d, cap=%d\n", data, len(data), cap(data))
fmt.Printf("s1: %v, len=%d, cap=%d\n", s1, len(s1), cap(s1))
fmt.Printf("s2: %v, len=%d, cap=%d\n", s2, len(s2), cap(s2))
// s2修改元素会影响s1和data
s2[0] = 999
fmt.Printf("\n修改s2[0]=999后:\n")
fmt.Printf("data: %v\n", data) // data[4]被改为999
fmt.Printf("s1: %v\n", s1) // s1[2]被改为999
fmt.Printf("s2: %v\n", s2) // s2[0]被改为999
// append可能导致断开关联
fmt.Println("\n--- append导致扩容 ---")
s3 := data[0:2] // [0, 1], len=2, cap=10
fmt.Printf("append前 s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))
// append未超过容量,仍共享
s3 = append(s3, 99)
fmt.Printf("append(99)后 s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))
fmt.Printf("data[2] = %d (被修改了!)\n", data[2])
// 超过容量后,分配新数组
s3 = append(s3, 100, 200, 300, 400, 500, 600, 700, 800)
fmt.Printf("大量append后 s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))
fmt.Printf("data不受影响: %v\n", data)
}
func main() {
// ========== 切片操作实战 ==========
fmt.Println("=== 切片操作实战 ===")
// 初始数据
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println("原始数据:", nums)
// 删除索引4的元素(值为5)
nums = removeElement(nums, 4)
fmt.Println("删除索引4后:", nums)
// 在索引2处插入100
nums = insertElement(nums, 2, 100)
fmt.Println("在索引2插入100:", nums)
// 过滤偶数
evens := filterEven(nums)
fmt.Println("偶数切片:", evens)
// ========== 切片作为栈 ==========
fmt.Println("\n=== 切片模拟栈 ===")
var stack []int
// 入栈
for i := 1; i <= 5; i++ {
stack = append(stack, i)
fmt.Printf("入栈 %d -> 栈: %v\n", i, stack)
}
// 出栈(从末尾弹出)
for len(stack) > 0 {
// 取出最后一个元素
top := stack[len(stack)-1]
stack = stack[:len(stack)-1]
fmt.Printf("出栈 %d -> 栈: %v\n", top, stack)
}
// ========== 底层原理演示 ==========
fmt.Println()
sliceInternals()
}
运行结果:
=== 切片操作实战 ===
原始数据: [1 2 3 4 5 6 7 8 9 10]
删除索引4后: [1 2 3 4 6 7 8 9 10]
在索引2插入100: [1 2 100 3 4 6 7 8 9 10]
偶数切片: [2 100 4 6 8 10]
=== 切片模拟栈 ===
入栈 1 -> 栈: [1]
入栈 2 -> 栈: [1 2]
入栈 3 -> 栈: [1 2 3]
入栈 4 -> 栈: [1 2 3 4]
入栈 5 -> 栈: [1 2 3 4 5]
出栈 5 -> 栈: [1 2 3 4]
出栈 4 -> 栈: [1 2 3]
出栈 3 -> 栈: [1 2]
出栈 2 -> 栈: [1]
出栈 1 -> 栈: []
=== 切片底层原理演示 ===
data: [0 1 2 3 4 5 6 7 8 9], len=10, cap=10
s1: [2 3 4], len=3, cap=8
s2: [3 4], len=2, cap=7
修改s2[0]=999后:
data: [0 1 2 3 999 5 6 7 8 9]
s1: [2 3 999]
s2: [3 999]
--- append导致扩容 ---
append前 s3: [0 1], len=2, cap=10
append(99)后 s3: [0 1 99], len=3, cap=10
data[2] = 99 (被修改了!)
大量append后 s3: [0 1 99 100 200 300 400 500 600 700 800], len=12, cap=20
data不受影响: [0 1 99 3 999 5 6 7 8 9]
3. 常见应用场景
场景1:批量处理数据(过滤与转换)
package main
import "fmt"
// 将字符串切片中的空字符串过滤掉,并转为大写
func cleanData(input []string) []string {
result := make([]string, 0, len(input))
for _, s := range input {
if s != "" {
// 转大写(简化示例,实际可用strings.ToUpper)
upper := ""
for _, c := range s {
if c >= 'a' && c <= 'z' {
upper += string(c - 32)
} else {
upper += string(c)
}
}
result = append(result, upper)
}
}
return result
}
func main() {
raw := []string{"hello", "", "world", "", "go", "lang"}
cleaned := cleanData(raw)
fmt.Println("清洗后:", cleaned) // [HELLO WORLD GO LANG]
}
场景2:实现动态队列
package main
import "fmt"
// Queue 使用切片实现简单的FIFO队列
type Queue struct {
items []string
}
// Enqueue 入队
func (q *Queue) Enqueue(item string) {
q.items = append(q.items, item)
}
// Dequeue 出队
func (q *Queue) Dequeue() (string, bool) {
if len(q.items) == 0 {
return "", false
}
item := q.items[0]
q.items = q.items[1:] // 移除第一个元素
return item, true
}
// Size 队列大小
func (q *Queue) Size() int {
return len(q.items)
}
func main() {
q := &Queue{}
q.Enqueue("任务A")
q.Enqueue("任务B")
q.Enqueue("任务C")
fmt.Printf("队列大小: %d\n", q.Size())
for q.Size() > 0 {
item, _ := q.Dequeue()
fmt.Println("处理:", item)
}
}
❓ 常见问题
Q1:var s []int 声明的切片是nil切片,能直接使用吗?
可以。nil切片的len和cap都为0,append操作完全正常。Go官方建议:如果切片可能为nil,使用前不需要额外的nil检查,直接append即可。
var s []int // nil切片
s = append(s, 1, 2) // 完全合法
fmt.Println(s) // [1 2]
Q2:切片截取s[2:5]会影响原切片吗?
会。截取产生的子切片与原切片共享底层数组,修改子切片的元素会反映到原切片。如果需要独立副本,使用copy:
original := []int{1, 2, 3, 4, 5}
sub := make([]int, 3)
copy(sub, original[2:5]) // 独立副本,互不影响
Q3:append后旧切片的值为什么变了?
当len(s) < cap(s)时,append直接在底层数组上写入新元素,不会创建新数组。如果此时有其他切片引用了同一底层数组的后续位置,就会看到"意外"的修改。解决方法:在append前用copy创建独立副本。
Q4:切片的容量是怎么增长的?
Go运行时根据切片当前容量决定新容量。通常策略是:容量小于1024时翻倍增长,大于1024时增长约1.25倍。具体策略可能随Go版本变化,不要依赖精确的增长规则。
📖 小节
- 数组
[N]T长度固定,是值类型,赋值/传参会复制整个数组 - 切片
[]T长度可变,是引用类型,底层引用一个数组 make([]T, len, cap)是创建切片的推荐方式,可预分配容量避免频繁扩容append返回新切片,必须接收返回值;当容量不足时会分配新底层数组- 切片截取
s[low:high]与原切片共享底层数组,修改会互相影响 copy(dst, src)用于创建切片的独立副本len()返回元素个数,cap()返回底层数组的容量- 切片的零值是
nil,可以直接append使用
📝 作业
练习1(⭐)
编写程序,创建一个包含10个整数的切片(值为1~10),然后:
- 打印切片的长度和容量
- 截取索引2到7的子切片,打印其内容、长度和容量
- 使用range遍历子切片,打印每个元素及其索引
练习2(⭐⭐)
编写一个unique函数,接收一个[]int切片,返回去重后的新切片(保持元素首次出现的顺序)。例如输入[1, 3, 2, 3, 1, 4, 2]应返回[1, 3, 2, 4]。
提示:可以用一个辅助切片记录已经出现过的元素。
练习3(⭐⭐⭐)
编写一个mergeSorted函数,接收两个已排序的[]int切片,合并为一个排序切片。要求:
- 时间复杂度为O(n),不能先合并再排序
- 例如输入
[1, 3, 5, 7]和[2, 4, 6, 8],返回[1, 2, 3, 4, 5, 6, 7, 8] - 思考:这个操作在归并排序中有什么作用?



