文章

Day08(切片 Slice 和 Range 遍历详解)

Go语言切片底层原理、扩容机制、range遍历最佳实践

Day08(切片 Slice 和 Range 遍历详解)

概述

切片(Slice)是 Go 语言中最常用的数据结构之一,它是数组的抽象封装,提供了动态、灵活的序列操作能力。本文将深入讲解切片的底层原理、扩容机制以及 range 遍历的使用技巧。

目录

  1. 切片基础
  2. 切片底层原理
  3. 扩容机制详解
  4. 常用操作
  5. Range 遍历
  6. 性能优化
  7. 常见陷阱

    切片基础

什么是切片?

切片是对数组的动态视图,它包含三个要素:

  • 指针(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 会触发扩容:

  1. 如果新容量 > 2倍旧容量: 使用新容量
  2. 如果旧容量 < 1024: 新容量 = 旧容量 × 2
  3. 如果旧容量 >= 1024: 新容量 = 旧容量 × 1.25 (每次增加 1/4)
  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遍历循环迭代 arrayslicechannelmap

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 进行授权