5·基础入门入门

数组与切片

arrayslicemakeappend

第5课:数组与切片

学习目标

完成本课学习后,你将能够:

  1. 理解Go语言中数组的声明、初始化和基本操作
  2. 掌握切片的概念及其与数组的核心区别
  3. 熟练使用 makeappend 函数操作切片
  4. 理解切片的工作原理,包括长度、容量和底层数组
  5. 为后续学习切片在并发编程中的安全使用打下基础

核心概念

1. 数组:固定大小的容器

想象一个储物柜,每个格子大小相同,总格子数在建造时就确定了。这就是数组——一个固定长度的、存放相同类型元素的数据结构。

// 声明并初始化一个长度为3的整型数组
var ages [3]int = [3]int{10, 20, 30}

数组长度是其类型的一部分,[3]int[4]int 是不同的类型,不能互相赋值。这个“僵化”的特性有时会带来不便。

2. 切片:灵活的“窗口”

切片是基于数组的抽象,它更像一个动态窗口,提供三个关键信息:

  • 指针:指向底层数组的某个元素
  • 长度(Length):切片当前包含的元素数量,用 len(s) 获取
  • 容量(Capacity):从切片起始位置到底层数组末尾的元素数量,用 cap(s) 获取
// 直接声明并初始化一个切片(Go会自动创建底层数组)
scores := []int{85, 90, 95}

// 从数组创建切片
numbers := [5]int{1, 2, 3, 4, 5}
slice1 := numbers[1:3] // 包含索引1和2,即[2, 3]

切片最强大的地方在于它可以动态增长,这是通过 append 函数实现的。

3. makeappend:切片的创建与增长

  • make:创建切片时预分配空间,适合明确知道容量范围的场景
  • append:向切片末尾追加元素,如果容量不足会自动扩容(通常创建新数组)
// 创建一个长度为0,容量为5的切片
data := make([]int, 0, 5)

// 追加元素
data = append(data, 10)
data = append(data, 20, 30)

关键点append 可能返回新的切片(当发生扩容时),所以结果必须赋值给原变量。

代码示例

示例1:数组与切片的基本操作

package main

import "fmt"

func main() {
    // 数组示例
    var fruits [3]string = [3]string{"苹果", "香蕉", "橙子"}
    fmt.Println("水果数组:", fruits)
    fmt.Println("第一个水果:", fruits[0])
    
    // 切片示例(直接创建)
    vegetables := []string{"西红柿", "黄瓜", "茄子"}
    fmt.Println("蔬菜切片:", vegetables)
    
    // 切片操作
    // 1. 获取部分切片
    subVeg := vegetables[1:3]
    fmt.Println("部分蔬菜:", subVeg) // [黄瓜, 茄子]
    
    // 2. 追加元素
    vegetables = append(vegetables, "土豆")
    fmt.Println("追加后:", vegetables)
    
    // 3. 检查长度和容量
    fmt.Printf("长度: %d, 容量: %d\n", len(vegetables), cap(vegetables))
}

示例2:深入理解切片的工作原理

package main

import "fmt"

func main() {
    // 创建底层数组
    array := [5]int{10, 20, 30, 40, 50}
    fmt.Println("原始数组:", array)
    
    // 创建两个切片(共享同一个底层数组)
    sliceA := array[1:3] // 索引1-2: [20, 30]
    sliceB := array[2:4] // 索引2-3: [30, 40]
    
    fmt.Println("切片A:", sliceA, "长度:", len(sliceA), "容量:", cap(sliceA))
    fmt.Println("切片B:", sliceB, "长度:", len(sliceB), "容量:", cap(sliceB))
    
    // 修改切片A会影响底层数组,进而影响切片B
    sliceA[0] = 999
    fmt.Println("修改切片A后:")
    fmt.Println("切片A:", sliceA)
    fmt.Println("切片B:", sliceB) // sliceB[0] 也会变成999
    fmt.Println("底层数组:", array)
    
    // 使用append可能导致扩容,创建新的底层数组
    sliceA = append(sliceA, 1, 2, 3, 4, 5) // 追加多个元素,超出原容量
    fmt.Println("append扩容后:")
    fmt.Println("切片A:", sliceA)
    fmt.Println("切片B:", sliceB) // 不受影响,因为扩容创建了新数组
    fmt.Println("底层数组:", array) // 不受影响
}

示例3:使用make创建切片并管理容量

package main

import "fmt"

func main() {
    // 预分配容量,避免频繁扩容
    data := make([]int, 0, 10) // 长度0,容量10
    
    // 填充数据
    for i := 0; i < 15; i++ {
        data = append(data, i*2)
        fmt.Printf("添加%d后 - 长度:%d, 容量:%d\n", i*2, len(data), cap(data))
    }
    
    fmt.Println("最终切片:", data)
}

实践练习

练习1:基础操作(简单)

创建一个存储学生姓名的切片,完成以下操作:

  1. 初始包含:"张三", "李四", "王五"
  2. 追加:"赵六", "钱七"
  3. 删除索引为2的元素(王五)
  4. 打印最终结果

预期输出:

[张三 李四 赵六 钱七]

练习2:切片切片与修改(中等)

给定切片 data := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

  1. 创建切片 firstHalf(前5个元素)
  2. 创建切片 lastHalf(后5个元素)
  3. firstHalf 的最后一个元素(5)改为 100
  4. 验证修改是否影响了原切片 data,并解释原因

练习3:并发场景模拟(挑战)

创建一个共享的整数切片,模拟两个并发任务:

  1. 任务A:循环100次,每次向切片追加一个随机数
  2. 任务B:循环100次,每次读取切片长度
  3. 运行程序,观察是否会出现问题(提示:切片并发读写不安全)
  4. 思考:为什么会出现问题?该如何解决?(提示:使用 sync.Mutex

常见错误

1. 误以为切片是值传递

// 错误:认为函数内的修改不会影响原切片
func modifySlice(s []int) {
    s = append(s, 100) // 这不会影响原切片
}

// 正确做法:返回修改后的切片
func modifySliceCorrect(s []int) []int {
    return append(s, 100)
}

2. 切片扩容后的误解

original := make([]int, 3, 3) // [0, 0, 0]
s1 := original[:2]            // 指向同一数组

s1 = append(s1, 100)          // 扩容!创建新数组
s1[0] = 999                   // 修改s1不影响original

// 此时:
// s1: [999, 0, 100](新数组)
// original: [0, 0, 0](原数组未变)

3. 切片索引越界

s := []int{1, 2, 3}
// 错误:访问超出范围的索引
// value := s[5] // 运行时panic

// 正确:先检查长度
if len(s) > 5 {
    value := s[5]
}

4. 并发使用切片不加保护

var sharedSlice []int

// 错误:多个goroutine同时读写会导致数据竞争
go func() {
    sharedSlice = append(sharedSlice, 1)
}()

go func() {
    length := len(sharedSlice) // 可能读到不一致的数据
}()

小结

关键要点回顾

  1. 数组 vs 切片

    • 数组:固定长度,类型的一部分,适合大小固定的场景
    • 切片:动态长度,引用类型,更常用且灵活
  2. 切片三要素

    • 指针:指向底层数组
    • 长度:当前元素数量
    • 容量:最大可存储元素数量
  3. 核心操作

    • 创建:[]T{...}make([]T, len, cap)
    • 追加:append(slice, elements...)
    • 切片:slice[low:high](左闭右开)
    • 复制:copy(dst, src)
  4. 重要特性

    • 切片是引用类型,函数间传递成本低
    • 共享切片可能共享底层数组,修改会相互影响
    • append 可能创建新的底层数组(扩容时)
    • 切片不是并发安全的,这是后续并发编程的重要考点

下一步学习预告

在下一课中,我们将学习 Map(哈希表),它是另一种强大的数据结构。同时,我们将开始探讨为什么在并发编程中,切片和Map都需要特殊的保护机制。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「Map 哈希表」 以巩固所学知识。