Files
test/chapters/chapter-4-revised.md

395 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第四章:函数与接口 —— 让代码“活”起来
> **👋 回顾一下:**
>
> 前三章,我们学会了基础语法、数据类型和数据结构。现在,你已经能写出逻辑清晰的代码了。
>
> **🤔 但是,代码不仅仅是“能跑”就行,还要“好维护”、“可复用”、“易扩展”。**
>
> 想象一下,如果你写了一个很长的函数,里面塞满了各种逻辑,别人(或者未来的你)想修改其中一小部分,是不是得小心翼翼,生怕改坏了其他地方?
>
> 这时候,我们需要**函数**来**拆分逻辑**,用**接口**来**解耦依赖**,用**闭包**来**保存状态**。
>
> **🎯 这一章,我们要掌握 Go 的“灵魂”:**
> 1. **函数进阶**:可变参数、命名返回值。
> 2. **闭包**:函数“记住”了它创建时的环境。
> 3. **defer**:优雅地处理“离场清理”。
> 4. **panic/recover**:处理“意外事故”。
> 5. **接口**:实现“多态”,让代码更灵活。
>
> **别担心,我会用大量的比喻和图解,让你轻松理解这些“高级”概念!**
---
## 4.1 函数进阶 —— 让参数更灵活
> **💡 想象一下:**
> 你去餐厅点菜,菜单上写着“炒饭”,但你可以选“加蛋”、“加火腿”、“加青菜”……参数越多,选择越灵活。
>
> 在 Go 中,函数也可以这样灵活!
### 📝 可变参数
```go
// 定义:最后一个参数可以是 ...T
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// 调用
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(10, 20)) // 30
fmt.Println(sum()) // 0没传参数也没问题
// 从切片展开
nums := []int{1, 2, 3, 4, 5}
fmt.Println(sum(nums...)) // 15
```
> **💡 老师的小揭秘:**
> * `...int` 在函数内部就是一个 `[]int` 切片。
> * 可变参数必须是**最后一个参数**。
> * 调用时用 `...` 展开切片,否则整个切片会被当作一个参数。
### 📝 命名返回值
```go
func divide(a, b float64) (result float64, err error) {
if b == 0 {
err = fmt.Errorf("除数不能为零")
return // 直接返回命名返回值
}
result = a / b
return // 直接返回命名返回值
}
```
> **💡 老师的小提醒:**
> * 命名返回值适合**返回值较多**或**需要明确语义**的场景。
> * 如果用了 `return` 不带值,会返回当前的命名返回值(注意不要滥用,容易让人困惑)。
---
## 4.2 闭包 (Closure) —— 函数“记住”了环境 ⭐ 核心重点
> **🤔 什么是闭包?**
> 想象一下,你有一个**魔法盒子**,里面装着一个函数。这个函数不仅能执行,还能**记住**它被创建时周围的变量。
>
> 这就是**闭包****函数 + 它创建时的环境**。
### 📖 闭包的原理(图解)
```mermaid
graph LR
subgraph "外部函数 createCounter"
count[变量 count=0]
Func[内部函数count++]
count -.-> Func
end
subgraph "闭包"
Closure[闭包 = Func + count 引用]
Func --- Closure
count --- Closure
end
style Closure fill:#ffeb3b,stroke:#333
style count fill:#4caf50,stroke:#333,color:#fff
```
> **💡 老师的小揭秘:**
> * 闭包返回的是**变量的引用**,不是值。
> * 每次调用外部函数,都会创建**新的变量**,所以每个闭包都有自己独立的“记忆”。
### 📝 闭包的经典例子:计数器
```go
func createCounter() func() int {
count := 0 // 局部变量
return func() int {
count++
return count
}
}
func main() {
c1 := createCounter()
c2 := createCounter()
fmt.Println(c1()) // 1
fmt.Println(c1()) // 2
fmt.Println(c1()) // 3
fmt.Println(c2()) // 1c2 是独立的count 从 0 开始)
}
```
> **⚠️ 常见陷阱:循环中的闭包**
```go
// 错误示范
func badLoop() {
funcs := make([]func(), 3)
for i := 0; i < 3; i++ {
funcs[i] = func() {
fmt.Println(i) // 捕获的是 i 的引用,循环结束后 i=3
}
}
for _, f := range funcs {
f() // 输出3 3 3
}
}
// 正确示范
func goodLoop() {
funcs := make([]func(), 3)
for i := 0; i < 3; i++ {
i := i // 创建新变量,捕获的是这个新变量
funcs[i] = func() {
fmt.Println(i)
}
}
for _, f := range funcs {
f() // 输出0 1 2
}
}
```
> **💡 老师的小提醒:**
> * 循环中用闭包时,**一定要把循环变量赋值给新变量**`i := i`),或者**用参数传递**`func(n int)`)。
> * 否则,所有闭包都会共享同一个循环变量,导致结果不符合预期。
> **🎮 动手练一练:**
> 1. 写一个 `createMultiplier(factor int)` 函数,返回一个函数,该函数能将输入乘以 factor。
> 2. 测试 `double := createMultiplier(2)` 和 `triple := createMultiplier(3)`。
---
## 4.3 defer 机制 —— 优雅地“离场清理”
> **💡 想象一下:**
> 你进房间前脱了鞋,离开时**一定**要穿上鞋再出门。
> 或者,你打开文件处理完后,**一定**要关闭文件。
>
> 在 Go 中,我们用 `defer` 来确保这些“离场清理”工作**一定会执行**,哪怕中间出错了。
### 📝 defer 的基本用法
```go
func processFile() {
file, _ := os.Open("data.txt")
defer file.Close() // 注册清理任务,函数返回前执行
// 处理文件...
// 即使中间 panicfile.Close() 也会执行
}
```
### 🔄 defer 的执行顺序LIFO后进先出
```go
func deferOrder() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("开始")
}
// 输出:
// 开始
// 3
// 2
// 1
```
> **💡 老师的小揭秘:**
> * `defer` 语句**立即执行**(参数求值),但函数调用**延迟到函数返回前**。
> * 多个 `defer` 按**后进先出**顺序执行(像叠盘子)。
### ⚠️ 常见陷阱:参数求值时机
```go
func deferArgs() {
i := 0
defer fmt.Println("i =", i) // 参数 i 在 defer 时求值,值为 0
i = 10
// 输出i = 0
}
func deferClosure() {
i := 0
defer func() {
fmt.Println("i =", i) // 闭包在 defer 执行时求值,值为 10
}()
i = 10
// 输出i = 10
}
```
> **💡 老师的小提醒:**
> * 普通函数调用的参数在 `defer` 时**立即求值**。
> * 闭包的变量在闭包**执行时求值**。
> * 如果需要延迟求值,用闭包!
### 🎮 动手练一练:
写一个函数,打开一个文件,写入数据,然后用 `defer` 确保文件关闭。尝试在写入后 `panic`,观察文件是否依然关闭。
---
## 4.4 panic 与 recover —— 处理“意外事故”
> **💡 想象一下:**
> 开车时遇到紧急情况,需要**紧急刹车**panic然后由**安全气囊**recover接住你避免车毁人亡。
>
> 在 Go 中:
> * `panic`:立即停止当前函数,开始**栈展开**(执行所有 defer
> * `recover`:在 `defer` 中调用,**捕获 panic**,让程序继续运行。
### 📝 panic 与 recover 的用法
```go
func safeDivide(a, b int) int {
defer func() {
if r := recover(); r != nil {
fmt.Println("捕获到 panic:", r)
}
}()
if b == 0 {
panic("除数不能为零")
}
return a / b
}
func main() {
result := safeDivide(10, 0)
fmt.Println("结果:", result) // 不会执行,因为 panic 了
}
```
> **💡 老师的小提醒:**
> * **不要滥用 panic**:优先返回错误(`error`)。
> * **panic 仅用于不可恢复的错误**:如配置加载失败、数据库连接失败等。
> * `recover` 必须在 `defer` 中调用,否则无效。
> **🎮 动手练一练:**
> 写一个函数,模拟读取配置文件,如果文件不存在则 `panic`。在调用处用 `recover` 捕获并打印错误信息。
---
## 4.5 接口 (Interfaces) —— 实现“多态” ⭐ 核心重点
> **💡 想象一下:**
> 你有各种电器(电视、冰箱、空调),它们都有**插头**。只要插头规格一样(接口),就能插到同一个插座上(调用方法),不用关心里面是什么电器。
>
> 这就是**接口****定义行为,不关心实现**。
### 📝 接口的定义与实现
```go
// 定义接口
type Speaker interface {
Speak() string
}
// 实现接口(隐式!不需要 implements 关键字)
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return d.Name + " 汪汪叫"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return c.Name + " 喵喵叫"
}
// 使用接口
func makeSound(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
makeSound(Dog{Name: "旺财"}) // 旺财 汪汪叫
makeSound(Cat{Name: "咪咪"}) // 咪咪 喵喵叫
}
```
> **💡 老师的小揭秘:**
> * **隐式实现**:只要类型实现了接口的所有方法,就自动实现该接口(鸭子类型)。
> * **小接口原则**:接口方法越少越好(如 `io.Reader` 只有一个 `Read` 方法)。
> * **接口定义在使用者一侧**:在调用方定义接口,而不是在实现方。
### 📖 接口的底层结构(图解)
```mermaid
graph LR
subgraph "接口变量 (iface/eface)"
Data[数据指针]
Type[类型信息]
Data -.-> Interface[接口变量]
Type -.-> Interface
end
subgraph "具体类型"
Concrete[Dog 结构体]
Concrete -.-> Data
end
subgraph "方法表"
Method[Speak 方法]
Type -.-> Method
end
style Interface fill:#ffeb3b,stroke:#333
style Concrete fill:#4caf50,stroke:#333,color:#fff
```
> **💡 老师的小揭秘:**
> * 接口变量包含**数据指针**和**类型信息**。
> * 调用接口方法时Go 会根据类型信息找到对应的方法。
### ⚠️ 常见陷阱nil 接口
```go
var s Speaker = nil
fmt.Println(s == nil) // true
var d *Dog = nil
var s2 Speaker = d
fmt.Println(s2 == nil) // false因为 s2 的类型是 *Dog不是 nil
```
> **💡 老师的小提醒:**
> * 接口为 `nil` 当且仅当**类型和值都为 nil**。
> * 不要直接比较接口和 `nil`,除非你确定类型也是 nil。
> **🎮 动手练一练:**
> 1. 定义一个 `Writer` 接口(`Write([]byte) (int, error)`)。
> 2. 实现 `FileWriter` 和 `MemoryWriter`。
> 3. 写一个函数 `saveData(w Writer, data []byte)`,用接口实现通用保存。
---
## 4.6 本章小结
> **🎯 我们学到了什么?**
> 1. **函数进阶**:可变参数、命名返回值。
> 2. **闭包**:函数 + 环境,注意循环陷阱。
> 3. **defer**LIFO 顺序,参数求值时机,清理资源。
> 4. **panic/recover**:紧急刹车,谨慎使用。
> 5. **接口**:隐式实现,小接口原则,多态。
>
> **🚀 下一步预告:**
> 学会了函数和接口,我们怎么让程序**同时处理多个任务**?怎么让 Goroutine 之间**安全通信**
>
> **下一章,我们要进入 Go 最强大的领域:并发编程!** 我们会学习 Goroutine、Channel、同步原语以及并发设计模式。准备好迎接“高并发”的世界了吗