Day08(切片 Slice 和 Range 遍历详解)
Go语言切片底层原理、扩容机制、range遍历最佳实践
Day08(切片 Slice 和 Range 遍历详解)
概述
切片(Slice)是 Go 语言中最常用的数据结构之一,它是数组的抽象封装,提供了动态、灵活的序列操作能力。本文将深入讲解切片的底层原理、扩容机制以及 range 遍历的使用技巧。
目录
什么是切片?
切片是对数组的动态视图,它包含三个要素:
- 指针(Pointer): 指向底层数组的起始位置
- 长度(Length): 切片中元素的数量
- 容量(Capacity): 从切片起始位置到底层数组末尾的元素数量
1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"
func main() {
// 创建切片
s := []int{1, 2, 3, 4, 5}
fmt.Printf("Slice: %v\n", s)
fmt.Printf("Length: %d\n", len(s)) // 5
fmt.Printf("Capacity: %d\n", cap(s)) // 5
}
切片 vs 数组
| 特性 | 数组 (Array) | 切片 (Slice) |
|---|---|---|
| 长度 | 固定 | 动态 |
| 类型 | [5]int |
[]int |
| 传递方式 | 值拷贝 | 引用(指针+len+cap) |
| 灵活性 | 低 | 高 |
| 使用频率 | 少 | 常用 |
1
2
3
4
5
6
7
// 数组 - 固定长度
var arr [5]int
arr[0] = 1
// 切片 - 动态长度
var slice []int
slice = append(slice, 1)
切片底层原理
切片的数据结构
1
2
3
4
5
type slice struct {
array unsafe.Pointer // 指向底层数组
len int // 长度
cap int // 容量
}
创建切片的多种方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"
func main() {
// 方式 1: 字面量创建
s1 := []int{1, 2, 3}
fmt.Printf("s1: %v, len: %d, cap: %d\n", s1, len(s1), cap(s1))
// 方式 2: make 创建
s2 := make([]int, 5) // len=5, cap=5
s3 := make([]int, 3, 10) // len=3, cap=10
fmt.Printf("s2: %v, len: %d, cap: %d\n", s2, len(s2), cap(s2))
fmt.Printf("s3: %v, len: %d, cap: %d\n", s3, len(s3), cap(s3))
// 方式 3: 从数组或切片截取
arr := [5]int{1, 2, 3, 4, 5}
s4 := arr[1:4] // [2, 3, 4]
fmt.Printf("s4: %v, len: %d, cap: %d\n", s4, len(s4), cap(s4))
}
输出:
1
2
3
4
s1: [1 2 3], len: 3, cap: 3
s2: [0 0 0 0 0], len: 5, cap: 5
s3: [0 0 0], len: 3, cap: 10
s4: [2 3 4], len: 3, cap: 4
扩容机制详解
扩容规则 (Go 1.18+)
当切片容量不足时,append 会触发扩容:
- 如果新容量 > 2倍旧容量: 使用新容量
- 如果旧容量 < 1024: 新容量 = 旧容量 × 2
- 如果旧容量 >= 1024: 新容量 = 旧容量 × 1.25 (每次增加 1/4)
- 内存对齐: 最终容量会根据元素类型进行对齐
1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"
func main() {
s := make([]int, 0, 1)
for i := 0; i < 10; i++ {
s = append(s, i)
fmt.Printf("len: %2d, cap: %2d\n", len(s), cap(s))
}
}
输出示例:
1
2
3
4
5
6
7
8
9
10
len: 1, cap: 1
len: 2, cap: 2 (×2)
len: 3, cap: 4 (×2)
len: 4, cap: 4
len: 5, cap: 8 (×2)
len: 6, cap: 8
len: 7, cap: 8
len: 8, cap: 8
len: 9, cap: 16 (×2)
len: 10, cap: 16
扩容时的内存分配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"
func main() {
// 场景 1: 扩容未超过原数组容量 - 共享底层数组
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[0:3] // [1, 2, 3], cap=5
s1 = append(s1, 100)
fmt.Printf("arr: %v\n", arr) // [1 2 3 100 5] - 影响了原数组!
// 场景 2: 扩容超过原数组容量 - 分配新数组
s2 := arr[0:3] // [1, 2, 3], cap=5
s2 = append(s2, 1, 2, 3) // 需要 cap=6, 超过原数组
fmt.Printf("arr: %v\n", arr) // [1 2 3 100 5] - 不受影响
fmt.Printf("s2: %v\n", s2) // [1 2 3 1 2 3]
}
⚠️ 重要: 切片可能共享底层数组,修改一个切片可能影响其他切片!
常用操作
切片截取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4, 5}
// 基本截取
fmt.Println(s[1:3]) // [2 3]
fmt.Println(s[:3]) // [1 2 3] (等同于 s[0:3])
fmt.Println(s[2:]) // [3 4 5] (等同于 s[2:len(s)])
fmt.Println(s[:]) // [1 2 3 4 5] (完整副本的视图)
// 注意:截取后的切片与原切片共享底层数组
s2 := s[1:3]
s2[0] = 100
fmt.Println(s) // [1 100 3 4 5] - 原切片被修改!
}
Append 操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"
func main() {
var s []int
// 追加单个元素
s = append(s, 1)
fmt.Println(s) // [1]
// 追加多个元素
s = append(s, 2, 3, 4)
fmt.Println(s) // [1 2 3 4]
// 追加另一个切片
s2 := []int{5, 6, 7}
s = append(s, s2...)
fmt.Println(s) // [1 2 3 4 5 6 7]
}
Copy 操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
func main() {
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
// copy 返回实际复制的元素个数
n := copy(dst, src)
fmt.Printf("dst: %v, copied: %d\n", dst, n) // [1 2 3], 3
// copy 不会扩容,只复制到 min(len(src), len(dst))
}
删除元素
Go 没有内置的删除函数,使用 append 组合实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4, 5}
// 删除索引为 2 的元素 (值为 3)
index := 2
s = append(s[:index], s[index+1:]...)
fmt.Println(s) // [1 2 4 5]
// 删除第一个元素
s = s[1:]
fmt.Println(s) // [2 4 5]
// 删除最后一个元素
s = s[:len(s)-1]
fmt.Println(s) // [2 4]
}
排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"fmt"
"sort"
)
func main() {
nums := []int{5, 2, 8, 1, 9, 3}
// 升序排序
sort.Ints(nums)
fmt.Println(nums) // [1 2 3 5 8 9]
// 降序排序
sort.Sort(sort.Reverse(sort.IntSlice(nums)))
fmt.Println(nums) // [9 8 5 3 2 1]
// 检查是否已排序
fmt.Println(sort.IntsAreSorted(nums)) // false
}
Range 遍历
range 是 Go 语言中用于遍历集合的关键字,支持数组、切片、Map、Channel 和字符串。
遍历切片和数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import "fmt"
func main() {
nums := []int{10, 20, 30, 40}
// 方式 1: 获取索引和值
for i, v := range nums {
fmt.Printf("index: %d, value: %d\n", i, v)
}
// 方式 2: 只要值,忽略索引
for _, v := range nums {
fmt.Printf("value: %d\n", v)
}
// 方式 3: 只要索引,忽略值
for i := range nums {
fmt.Printf("index: %d\n", i)
}
}
遍历 Map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"
func main() {
m := map[string]int{
"Alice": 25,
"Bob": 30,
"Carol": 35,
}
// 遍历 Map(无序)
for key, value := range m {
fmt.Printf("%s: %d\n", key, value)
}
// 只遍历 key
for key := range m {
fmt.Println(key)
}
}
⚠️ 注意: Map 遍历顺序是不确定的,每次运行可能不同
遍历字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"
func main() {
str := "Hello 世界"
// 按 byte 遍历(不适合中文)
fmt.Println("By bytes:")
for i := 0; i < len(str); i++ {
fmt.Printf("%d ", str[i])
}
fmt.Println()
// 按 rune 遍历(推荐)
fmt.Println("\nBy runes:")
for i, char := range str {
fmt.Printf("index: %d, char: %c\n", i, char)
}
}
输出:
1
2
3
4
5
6
7
8
9
10
11
12
By bytes:
72 101 108 108 111 32 228 184 150 231 149 140
By runes:
index: 0, char: H
index: 1, char: e
index: 2, char: l
index: 3, char: l
index: 4, char: o
index: 5, char:
index: 6, char: 世
index: 9, char: 界
遍历 Channel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import "fmt"
func main() {
ch := make(chan int, 3)
// 启动 goroutine 发送数据
go func() {
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch) // 必须关闭,否则 range 会阻塞
}()
// 遍历 channel
for value := range ch {
fmt.Printf("Received: %d\n", value)
}
fmt.Println("Channel closed")
}
性能优化
1. 预分配容量
如果知道切片的大致大小,预先分配容量可以避免多次扩容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main
import (
"fmt"
"time"
)
func main() {
size := 1000000
// ❌ 慢:未预分配,频繁扩容
start := time.Now()
s1 := make([]int, 0)
for i := 0; i < size; i++ {
s1 = append(s1, i)
}
fmt.Printf("Without pre-allocation: %v\n", time.Since(start))
// ✅ 快:预分配容量
start = time.Now()
s2 := make([]int, 0, size)
for i := 0; i < size; i++ {
s2 = append(s2, i)
}
fmt.Printf("With pre-allocation: %v\n", time.Since(start))
}
2. 使用 copy 代替循环
1
2
3
4
5
6
7
// ❌ 慢:循环复制
for i, v := range src {
dst[i] = v
}
// ✅ 快:使用 copy
copy(dst, src)
3. 避免不必要的切片复制
1
2
3
4
5
6
// ❌ 创建新切片
newSlice := make([]int, len(oldSlice))
copy(newSlice, oldSlice)
// ✅ 直接引用(如果不需要修改)
newSlice := oldSlice
常见陷阱
1. 共享底层数组导致的意外修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"
func main() {
s1 := []int{1, 2, 3, 4, 5}
s2 := s1[1:3] // [2, 3]
s2[0] = 100
fmt.Println(s1) // [1 100 3 4 5] - s1 被意外修改!
// ✅ 解决方案:使用 copy 创建独立副本
s3 := make([]int, len(s2))
copy(s3, s2)
s3[0] = 200
fmt.Println(s1) // [1 100 3 4 5] - 不受影响
}
2. 内存泄漏:大切片持有小视图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
func getData() []byte {
bigData := make([]byte, 1024*1024) // 1MB
// ... 填充数据 ...
return bigData[0:10] // 只返回 10 bytes,但持有 1MB 内存!
}
func main() {
small := getData()
fmt.Println(len(small), cap(small)) // 10 1048576
}
✅ 解决方案:
1
2
3
4
5
6
func getDataFixed() []byte {
bigData := make([]byte, 1024*1024)
result := make([]byte, 10)
copy(result, bigData[:10])
return result // 只持有 10 bytes
}
3. Append 后忘记赋值
1
2
3
4
5
6
s := []int{1, 2, 3}
append(s, 4) // ❌ 错误:返回值未赋值
fmt.Println(s) // [1 2 3] - 未改变
s = append(s, 4) // ✅ 正确
fmt.Println(s) // [1 2 3 4]
小结
- 切片是动态数组的抽象,包含指针、长度、容量三个字段
- 扩容规则:<1024 时 ×2,>=1024 时 ×1.25
- 切片可能共享底层数组,修改时需谨慎
range可遍历切片、Map、Channel、字符串- 预分配容量可显著提升性能
- 注意避免内存泄漏和意外修改
掌握切片是 Go 编程的核心技能之一! 🎯
range
for遍历循环迭代array、slice、channel、map
1
2
3
for k ,v := range Type{
fmt.Println(k,v)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main
import (
"github.com/Cc360428/HelpPackage/UtilsHelp/logs"
)
func main() {
// array
array := []int{1, 2, 3, 4}
for k, v := range array {
logs.Info(k, v)
}
slice := make([]int, 3)
for k, v := range slice {
logs.Info(k, v)
}
// slice
channel := make(chan int, 2)
go func() {
for i := 1; i <= 3; i++ {
channel <- 8 * i
}
defer close(channel)
// !close(channel) --> fatal error: all goroutines are asleep - deadlock!
}()
// channel
for k := range channel {
logs.Info("channel", k)
}
// map
mapType := make(map[string]interface{})
mapType["name"] = "lcc"
mapType["age"] = 18
for k, v := range mapType {
logs.Info(k, v)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
2020/04/25 09:47:23.485 [I] [range.go:11] 0 1
2020/04/25 09:47:23.510 [I] [range.go:11] 1 2
2020/04/25 09:47:23.510 [I] [range.go:11] 2 3
2020/04/25 09:47:23.510 [I] [range.go:11] 3 4
2020/04/25 09:47:23.510 [I] [range.go:15] 0 0
2020/04/25 09:47:23.510 [I] [range.go:15] 1 0
2020/04/25 09:47:23.510 [I] [range.go:15] 2 0
2020/04/25 09:47:23.510 [I] [range.go:28] channel 8
2020/04/25 09:47:23.510 [I] [range.go:28] channel 16
2020/04/25 09:47:23.511 [I] [range.go:28] channel 24
2020/04/25 09:47:23.511 [I] [range.go:35] name lcc
2020/04/25 09:47:23.511 [I] [range.go:35] age 18
本文由作者按照
CC BY 4.0
进行授权