From 898083712a35886628ef8e88371fb092281c1006 Mon Sep 17 00:00:00 2001 From: openclaw Date: Tue, 24 Mar 2026 01:09:02 +0000 Subject: [PATCH] Revised Chapter 3: Data Structures - Friendly Tone, Natural Transitions, Analogies, and Interactive Exercises --- chapters/chapter-3-revised.md | 322 ++++++++++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 chapters/chapter-3-revised.md diff --git a/chapters/chapter-3-revised.md b/chapters/chapter-3-revised.md new file mode 100644 index 0000000..09cba7b --- /dev/null +++ b/chapters/chapter-3-revised.md @@ -0,0 +1,322 @@ +# 第三章:数据结构详解 —— 像搭积木一样组织数据 + +> **👋 回顾一下:** +> +> 前两章,我们学会了 Go 的基础语法:怎么声明变量、怎么判断、怎么循环。现在,你已经能写一些简单的程序了,比如计算器、猜数字游戏。 +> +> **🤔 但是,现实世界的数据往往更复杂……** +> +> 想象一下,你要管理一个班级的学生信息: +> * 一个学生有:姓名、年龄、成绩……(这需要**结构体**) +> * 一个班级有:50 个学生……(这需要**数组/切片**) +> * 你想快速通过学号查找学生……(这需要**映射**) +> +> 如果只用简单的变量,代码会变得非常臃肿、难以维护。这时候,我们就需要**数据结构**来帮我们**组织、存储、查找**数据。 +> +> **🎯 这一章,我们要认识 Go 的四大“数据容器”:** +> 1. **数组 (Array)**:固定长度的“盒子”。 +> 2. **切片 (Slice)**:动态长度的“魔盒”(**重点!Go 里最常用的**)。 +> 3. **映射 (Map)**:键值对“字典”,快速查找。 +> 4. **结构体 (Struct)**:自定义“包裹”,把相关数据打包。 +> +> **别担心,我会用大量的生活类比和图解,让你轻松理解!** + +--- + +## 3.1 数组 (Array) —— 固定长度的“盒子” + +> **💡 想象一下:** +> 你有一个**固定大小**的鞋盒,只能放 5 双鞋。 +> * 如果你只放了 3 双,剩下 2 个格子是空的(零值)。 +> * 如果你放了 6 双,**放不下了!** 必须换个大盒子。 +> +> 在 Go 中,**数组**就是这种**固定长度**的容器。 + +### 📝 数组的声明与初始化 + +```go +// 声明一个长度为 5 的整数数组,默认全是 0 +var scores [5]int + +// 初始化时指定值 +scores := [5]int{90, 85, 78, 92, 88} + +// 让 Go 自动推断长度 +nums := [...]int{1, 2, 3, 4, 5} // 长度是 5 +``` + +> **⚠️ 老师的小提醒:** +> * 数组长度是**类型的一部分**!`[5]int` 和 `[10]int` 是**不同类型**,不能互相赋值。 +> * 数组是**值类型**:赋值或传参时,会**完整复制**整个数组(如果数组很大,效率低)。 +> * **实际开发中,数组用得很少**,因为长度固定太死板了。别急,下一节我们介绍更灵活的“魔盒”——切片! + +> **🎮 动手练一练:** +> 声明一个长度为 3 的字符串数组,存储你的三个好朋友的名字,并打印出来。 + +--- + +## 3.2 切片 (Slice) —— 动态长度的“魔盒” ⭐ 核心重点 + +> **🤔 为什么需要切片?** +> 数组的固定长度太不灵活了。如果今天来了新同学,数组装不下怎么办?如果退学了,数组空着又浪费。 +> +> **切片**就是为了解决这个问题而生的!它像**魔盒**一样,**长度可以动态变化**,而且底层还是基于数组的(高效)。 + +### 📖 切片的底层结构(图解) + +切片不是数组,它是一个**描述符**,包含三个部分: +1. **指针 (ptr)**:指向底层数组的某个位置。 +2. **长度 (len)**:当前切片有多少个元素。 +3. **容量 (cap)**:从指针开始,到底层数组末尾还能装多少个元素。 + +```mermaid +graph LR + subgraph "切片描述符" + S[Slice: len=3, cap=5] + Ptr[指针] + Len[长度:3] + Cap[容量:5] + S --- Ptr + S --- Len + S --- Cap + end + + subgraph "底层数组" + A[数组] + E1[元素 0] + E2[元素 1] + E3[元素 2] + E4[元素 3] + E5[元素 4] + A --- E1 + A --- E2 + A --- E3 + A --- E4 + A --- E5 + + Ptr -.-> E1 + end + + style S fill:#ffeb3b,stroke:#333 + style A fill:#2196f3,stroke:#333,color:#fff +``` + +> **💡 老师的小揭秘:** +> * **len**:你目前能看到几个元素。 +> * **cap**:你还能往里面塞几个元素,不用重新分配内存。 +> * **指针**:切片只是底层数组的“视图”,修改切片会影响底层数组! + +### 📝 切片的创建 + +```go +// 1. 从数组创建 +arr := [5]int{1, 2, 3, 4, 5} +s1 := arr[1:4] // [2, 3, 4], len=3, cap=4 (从索引 1 到末尾) +s2 := arr[:3] // [1, 2, 3], len=3, cap=5 + +// 2. 用 make 创建(最常用) +s3 := make([]int, 5) // len=5, cap=5, 元素全为 0 +s4 := make([]int, 3, 10) // len=3, cap=10, 元素全为 0 + +// 3. 直接初始化 +s5 := []int{10, 20, 30} // len=3, cap=3 +``` + +### 🔄 切片的扩容机制(append) + +当你用 `append` 添加元素时,如果**长度 < 容量**,直接放进去;如果**长度 == 容量**,Go 会**重新分配一个更大的数组**,把旧数据复制过去。 + +```go +s := make([]int, 0, 5) // 初始容量 5 + +for i := 1; i <= 10; i++ { + s = append(s, i) + fmt.Printf("i=%d, len=%d, cap=%d\n", i, len(s), cap(s)) +} +``` + +**输出示例**: +``` +i=1, len=1, cap=5 +i=2, len=2, cap=5 +... +i=5, len=5, cap=5 +i=6, len=6, cap=10 <-- 容量翻倍了! +i=7, len=7, cap=10 +... +``` + +> **💡 老师的小技巧:** +> * 如果你知道大概要存多少个元素,**提前指定容量**(`make([]T, 0, expectedSize)`),可以避免多次扩容,提升性能。 +> * **切片是引用类型**:赋值、传参时,只复制描述符(指针、len、cap),**不复制底层数组**。所以修改切片会影响原数据! + +### ⚠️ 常见陷阱:共享底层数组 + +```go +arr := []int{1, 2, 3, 4, 5} +s1 := arr[1:3] // [2, 3] +s2 := arr[2:4] // [3, 4] + +s1[0] = 99 +fmt.Println(arr) // [1, 99, 3, 4, 5] <-- arr 也被修改了! +fmt.Println(s2) // [99, 4] <-- s2 也受影响! +``` + +> **💡 老师的小提醒:** +> 切片之间如果共享底层数组,修改一个会影响其他!**如果不需要共享,用 `append` 创建新切片**: +> ```go +> sNew := append([]int(nil), s1...) // 复制一份 +> ``` + +> **🎮 动手练一练:** +> 1. 创建一个长度为 0、容量为 10 的切片。 +> 2. 循环 append 1 到 15,观察 len 和 cap 的变化。 +> 3. 尝试修改切片中的元素,观察对原数组的影响。 + +--- + +## 3.3 映射 (Map) —— 快速查找的“字典” + +> **💡 想象一下:** +> 你想查“苹果”的价格,如果有一张表: +> * 水果 -> 价格 +> * 苹果 -> 5 元 +> * 香蕉 -> 3 元 +> +> 你只需要知道“苹果”,就能立刻找到价格,不用一个个遍历。这就是**映射 (Map)**! + +### 📝 映射的创建与操作 + +```go +// 1. 创建 +prices := make(map[string]int) +prices["apple"] = 5 +prices["banana"] = 3 + +// 2. 字面量初始化 +scores := map[string]int{ + "Alice": 95, + "Bob": 88, +} + +// 3. 读取 +price := prices["apple"] +fmt.Println(price) // 5 + +// 4. 检查键是否存在(重要!) +if val, ok := prices["orange"]; ok { + fmt.Println("存在,价格:", val) +} else { + fmt.Println("不存在") +} + +// 5. 删除 +delete(prices, "banana") + +// 6. 遍历(无序!) +for fruit, price := range prices { + fmt.Printf("%s: %d\n", fruit, price) +} +``` + +> **⚠️ 老师的小提醒:** +> * **无序**:每次遍历的顺序可能不同,这是 Go 故意设计的(防止依赖顺序)。 +> * **nil 映射不能写入**:`var m map[string]int` 是 nil,写入会 panic。必须 `make` 或字面量初始化。 +> * **并发不安全**:多个 Goroutine 同时读写同一个 Map 会 panic。需要加锁或用 `sync.Map`。 + +> **🎮 动手练一练:** +> 1. 创建一个 Map,存储你喜欢的 3 种水果和价格。 +> 2. 尝试读取一个不存在的键,用 `ok` 模式判断。 +> 3. 遍历 Map,打印所有水果。 + +--- + +## 3.4 结构体 (Struct) —— 自定义“包裹” + +> **💡 想象一下:** +> 你要描述一个“学生”,有姓名、年龄、成绩……这些属性属于同一个“实体”。在 Go 中,用**结构体**来打包。 + +### 📝 结构体的定义与初始化 + +```go +// 定义 +type Student struct { + Name string + Age int + Score float64 +} + +// 初始化 1:字段名初始化(推荐) +s1 := Student{ + Name: "Alice", + Age: 20, + Score: 95.5, +} + +// 初始化 2:位置初始化(不推荐,易错) +s2 := Student{"Bob", 22, 88.0} + +// 初始化 3:部分初始化 +s3 := Student{Name: "Charlie"} // Age=0, Score=0 +``` + +### 🔧 结构体方法 + +```go +// 给结构体添加方法 +func (s Student) Pass() bool { + return s.Score >= 60 +} + +func (s *Student) AddScore(delta float64) { + s.Score += delta // 指针接收者可以修改原结构体 +} +``` + +> **💡 老师的小揭秘:** +> * **值接收者**:`func (s Student)`,方法内修改不影响原结构体。 +> * **指针接收者**:`func (s *Student)`,方法内修改会影响原结构体。 +> * **推荐**:如果方法需要修改结构体,用指针接收者;如果只读取,用值接收者(小结构体)或指针接收者(大结构体,避免复制)。 + +### 🧩 结构体嵌入(组合) + +Go 没有“继承”,但可以用**嵌入**实现类似效果。 + +```go +type Person struct { + Name string + Age int +} + +type Student struct { + Person // 嵌入 Person + Score float64 +} + +s := Student{ + Person: Person{Name: "Alice", Age: 20}, + Score: 95.5, +} + +fmt.Println(s.Name) // 直接访问嵌入字段 +``` + +> **🎮 动手练一练:** +> 1. 定义一个 `Book` 结构体(书名、作者、价格)。 +> 2. 添加一个方法 `IsExpensive()`,判断价格是否大于 50。 +> 3. 创建几个 Book 实例,调用方法。 + +--- + +## 3.5 本章小结 + +> **🎯 我们学到了什么?** +> 1. **数组**:固定长度,少用。 +> 2. **切片**:动态长度,Go 的核心!理解 `len`、`cap`、底层数组。 +> 3. **映射**:键值对,快速查找,注意无序和并发安全。 +> 4. **结构体**:自定义类型,组合数据,支持方法。 +> +> **🚀 下一步预告:** +> 学会了数据结构,我们怎么让这些数据“动起来”?怎么复用代码?怎么实现多态? +> +> **下一章,我们要进入 Go 的“灵魂”:函数与接口!** 我们会学习闭包、defer、panic/recover,以及接口如何实现“多态”。准备好迎接更强大的功能了吗?