Initial commit

This commit is contained in:
Docker7530
2026-03-01 01:43:46 +08:00
commit c6125c117b
3840 changed files with 415340 additions and 0 deletions
+477
View File
@@ -0,0 +1,477 @@
# 0. 序言:Go 的并发哲学
Go 语言在并发编程领域推崇一个核心哲学:
> “不要通过共享内存来通信,而应该通过通信来共享内存。”
>
> "Don't communicate by sharing memory, share memory by communicating."
这句话的含义是,相比于传统并发模型中使用锁(`sync.Mutex` 等)来保护共享数据(共享内存),Go 更推荐使用 **Channel** 来在 Goroutine 之间传递数据(通信),从而自然地避免数据竞争问题。
- **精炼说明**: 这并不是说锁在 Go 中是无用的。锁在保护状态(State)时非常重要,而 Channel 在传递数据和同步事件(Communication and Synchronization)时更具优势。选择哪种工具取决于具体场景。
# 1. Channel 基础
## 1.1 什么是 Channel
Channel(通道)是 Goroutine 之间的通信管道。您可以把它想象成一个传送带,一个 Goroutine 将数据放到传送带的一端,另一个 Goroutine 从另一端取走。这确保了数据的安全传递。
## 1.2 创建与基本操作
Channel 是一种引用类型,需要使用 `make` 函数来创建。
- **声明**: `var ch chan int`
- **创建**: `ch = make(chan int)` (创建一个类型为 `int` 的无缓冲通道)
- **发送 (存)**: `ch <- 10` (将值 `10` 发送到通道 `ch`)
- **接收 (取)**: `data := <-ch` (从通道 `ch` 接收值并赋给 `data`)
## 1.3 基础使用示例
**所有 Channel 的操作都必须发生在至少两个或以上的 Goroutine 中**,否则会因阻塞而导致死锁。`main` 函数本身运行在一个主 Goroutine 中。
下面的代码演示了主 Goroutine 如何等待一个子 Goroutine 完成任务后发出的“完成”信号。
```go
package main
import (
"fmt"
"time"
)
func main() {
// 1. 创建一个用于通信的 bool 类型通道
// make(chan bool) 创建的是一个无缓冲通道
done := make(chan bool)
// 2. 启动一个新的 goroutine 来执行耗时任务
go func() {
fmt.Println("子 goroutine 开始执行任务...")
// 模拟工作
for i := 0; i < 5; i++ {
fmt.Println("goroutine-", i)
time.Sleep(500 * time.Millisecond)
}
fmt.Println("子 goroutine 任务完成。")
// 3. 任务完成后,向通道发送一个信号
done <- true
}()
// 4. 主 goroutine 在这里阻塞,等待从 `done` 通道接收信号
// 一旦子 goroutine 向 `done` 中写入数据,这里就会解除阻塞
<-done
fmt.Println("主 goroutine 收到完成信号,程序退出。")
}
```
# 2. Channel 的核心特性:阻塞
默认情况下,Channel 的发送和接收操作都是 **阻塞** 的。这是一种强大的同步机制。
- **发送阻塞**: 当一个 Goroutine 向无缓冲 Channel 发送数据时,它会**阻塞**,直到另一个 Goroutine 从该 Channel 接收数据。
- **接收阻塞**: 当一个 Goroutine 尝试从一个空的 Channel 接收数据时,它会**阻塞**,直到另一个 Goroutine 向该 Channel 发送数据。
这种“成对”的同步特性意味着,每一次通过无缓冲 Channel 的通信,都发生在发送方和接收方都准备好的那一刻。
## WaitGroup` vs `Channel`
在等待多个 Goroutine 完成的场景下,除了用 Channel,`sync.WaitGroup` 也是常用工具。
- **`sync.WaitGroup`**: 主要用于等待一组 Goroutine **全部完成**。它是一个计数器,不关心 Goroutine 的执行结果,只关心“是否做完”。
- **`Channel`**: 不仅可以等待 Goroutine 完成,还可以**接收它返回的数据**。功能更强大,但用于纯粹的等待时,代码可能比 `WaitGroup` 稍显繁琐。
# 3. 常见陷阱:死锁 (Deadlock)
死锁是指所有 Goroutine 都在互相等待,但谁也无法继续执行,导致程序永久挂起。这是使用 Channel 时最需要警惕的问题。
## 3.1 什么是死锁?
当一个 Goroutine 对一个 Channel 进行操作(发送或接收),但**永远没有**其他 Goroutine 来与之配对(接收或发送),就会发生死lock。
## 3.2 死锁的常见场景
**场景一:在单 Goroutine 中同时读写**
```go
package main
// fatal error: all goroutines are asleep - deadlock!
func main() {
// 创建一个无缓冲通道
ch := make(chan int)
// 错误!main goroutine 在这里发送数据,但被立即阻塞。
// 因为没有其他 goroutine 来接收数据,所以这个阻塞永远无法解除。
ch <- 10 // 程序在此处死锁
// 这行代码永远不会执行
// <-ch
}
```
**场景二:缓冲通道写入超出容量**
```go
package main
import "fmt"
// fatal error: all goroutines are asleep - deadlock!
func main() {
// 创建一个容量为 2 的缓冲通道
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println("已成功发送 2 个数据到缓冲通道")
// 错误!缓冲区已满(容量为 2),main goroutine 尝试再次发送。
// 由于没有其他 goroutine 从通道中取出数据,发送操作将被阻塞,导致死锁。
ch <- 3 // 程序在此处死锁
fmt.Println("这行代码永远不会执行到")
}
```
**场景三:从空缓冲通道读取**
```go
package main
import "fmt"
// fatal error: all goroutines are asleep - deadlock!
func main() {
ch := make(chan int, 2)
ch <- 1
x := <-ch // 正常,取出数据 1
fmt.Printf("成功取出数据: %d\n", x)
// 错误!通道中已无数据,main goroutine 尝试再次读取。
// 由于没有其他 goroutine 往通道中放入数据,读取操作将被阻塞,导致死锁。
x = <-ch // 程序在此处死锁
fmt.Println("这行代码永远不会执行到")
}
```
---
# 4. 关闭 Channel
## 4.1 为何及如何关闭
`close(ch)` 用于关闭一个 Channel,它向接收方发出一个明确信号:**“通道中不会再有新的数据发送过来了”**。
- **关闭原则:**
1. **由发送方关闭**:永远不要让接收方关闭 Channel,因为接收方无法知道发送方是否还会发送数据。让发送方在确定所有数据都已发送完毕后关闭 Channel,是最安全的实践。
2. 关闭已关闭的 Channel 会引发 `panic`
3. 向已关闭的 Channel 发送数据会引发 `panic`
4. 从已关闭的 Channel 接收数据不会阻塞。会立即返回该 Channel 类型的零值。
## 4.2 安全地接收数据
当接收方不确定 Channel 是否关闭时,可以使用多重返回值来判断。
`data, ok := <-ch`
- `ok``true`:表示成功从 Channel 中读取到数据 `data`
- `ok``false`:表示 Channel 已被关闭,且其中已无数据可读。此时 `data` 是该 Channel 类型的零值(如 `int``0``string``""`)。
```go
package main
import (
"fmt"
"time"
)
// producer 函数负责生产数据并发送到通道,完成后关闭通道
func producer(ch chan int) {
for i := 0; i < 5; i++ {
fmt.Printf("发送数据: %d\n", i)
ch <- i
time.Sleep(200 * time.Millisecond)
}
fmt.Println("所有数据已发送完毕,关闭通道。")
close(ch) // 发送方在发送完所有数据后关闭通道
}
func main() {
ch1 := make(chan int)
go producer(ch1)
// 使用 for 循环和 ok 判断来消费数据
for {
// ok 模式可以安全地判断通道是否已关闭
data, ok := <-ch1
if !ok {
fmt.Println("通道已关闭,读取结束。")
break // 退出循环
}
fmt.Printf("接收到数据: %d\n", data)
}
}
```
## 4.3 使用 `for range` 遍历 Channel
使用 `for range` 循环可以更优雅地从 Channel 中读取数据。该循环会自动监听 Channel,直到 Channel 被关闭并取完所有值后,循环会自动结束。
```go
package main
import (
"fmt"
"time"
)
// test7 函数(保持原名)生产数据并发送,完成后关闭通道
func test7(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
}
// 发送方负责关闭
close(ch)
}
func main() {
ch1 := make(chan int)
go test7(ch1)
// for range 会自动处理通道的关闭状态。
// 当通道 ch1 被关闭且缓冲区为空时,循环会自动退出。
// 这比使用 `data, ok := <-ch1` 的无限循环更简洁。
fmt.Println("开始使用 for range 接收数据...")
for data := range ch1 {
fmt.Printf("接收到: %d\n", data)
time.Sleep(200 * time.Millisecond) // 模拟消费耗时
}
fmt.Println("接收循环结束,主程序退出。")
}
```
# 5. 缓冲 Channel (Buffered Channel)
## 5.1 定义与特性
在创建 Channel 时,可以指定一个容量,这就是缓冲 Channel。
`ch := make(chan int, 3)` // 创建一个容量为 3 的 int 类型缓冲通道
- **发送**: 向缓冲 Channel 发送数据时,只有在**缓冲区已满**的情况下才会阻塞。
- **接收**: 从缓冲 Channel 接收数据时,只有在**缓冲区为空**的情况下才会阻塞。
缓冲 Channel 是一种**异步**通信方式,可以减少 Goroutine 间因同步阻塞而导致的等待,有助于提升性能,尤其是在生产者和消费者速度不匹配的场景下。
## 5.2 正确使用示例
```go
package main
import (
"fmt"
"time"
)
func main() {
// 创建一个容量为 3 的缓冲通道
// 这意味着发送方可以连续发送 3 个值而不会被阻塞
ch := make(chan int, 3)
go func() {
defer close(ch) // 确保 goroutine 退出时关闭通道
for i := 1; i <= 5; i++ {
fmt.Printf("发送方: 准备发送 %d\n", i)
ch <- i
fmt.Printf("发送方: 已发送 %d\n", i)
}
fmt.Println("发送方: 所有数据发送完毕。")
}()
// 主 goroutine (接收方) 稍等一下,让发送方先填满缓冲区
fmt.Println("接收方: 等待 2 秒...")
time.Sleep(2 * time.Second)
fmt.Println("接收方: 开始接收数据...")
// 使用 for range 消费通道中的所有数据
for val := range ch {
fmt.Printf("接收方: 收到 %d\n", val)
// 模拟消费数据的耗时
time.Sleep(500 * time.Millisecond)
}
fmt.Println("接收方: 通道已关闭,所有数据接收完毕。")
}
```
---
# 6. 高级用法
## 6.1 定向 Channel (Directional Channel)
为了增强代码的类型安全和可读性,可以将 Channel 限制为**只发送**或**只接收**。这在函数签名中特别有用,可以明确规定函数对 Channel 的操作权限。
- **只发送通道**: `chan<- int`
- **只接收通道**: `<-chan int`
```go
package main
import (
"fmt"
"time"
)
// writeOnly 函数被限制为只能向通道中写入数据
// 参数类型 `chan<- int` 明确了其职责,防止误操作
func writeOnly(ch chan<- int) {
fmt.Println("开始写入数据...")
for i := 0; i < 3; i++ {
val := (i + 1) * 100
ch <- val
fmt.Printf("写入: %d\n", val)
}
close(ch) // 写入者负责关闭
}
// readOnly 函数被限制为只能从通道中读取数据
// 参数类型 `<-chan int` 保证了函数内部不会污染通道
func readOnly(ch <-chan int) {
fmt.Println("开始读取数据...")
for data := range ch {
fmt.Printf("读取到: %d\n", data)
}
fmt.Println("读取完毕。")
}
func main() {
// 创建一个双向通道
ch1 := make(chan int)
// 当 ch1 传递给函数时,会自动转换为指定的单向类型
go writeOnly(ch1)
go readOnly(ch1)
// 等待两个 goroutine 执行完毕
// 这里的等待逻辑可以更健壮,例如使用 WaitGroup
time.Sleep(1 * time.Second)
fmt.Println("程序执行完毕。")
}
```
## 6.2 `select` 语句
`select` 语句让一个 Goroutine 可以**同时等待多个 Channel 操作**,其行为类似于 `switch` 语句,但每个 `case` 必须是一个 Channel 操作。
- **规则 1**: `select` 会阻塞,直到其中一个 `case` 的 Channel 操作就绪(即可读或可写)。
- **规则 2**: 如果有多个 `case` 同时就绪,`select` 会**随机选择一个**执行。
- **规则 3**: default` 子句:如果 `select` 中包含 `default`,它会使 `select` 变为**非阻塞**的。如果没有任何 `case` 就绪,`default` 会被立即执行。
```go
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
// Goroutine 1: 2秒后向 ch1 发送消息
go func() {
time.Sleep(2 * time.Second)
ch1 <- "来自 ch1 的消息"
}()
// Goroutine 2: 1秒后向 ch2 发送消息
go func() {
time.Sleep(1 * time.Second)
ch2 <- "来自 ch2 的消息"
}()
fmt.Println("等待消息...")
// select 会等待 ch1 或 ch2 中任意一个先准备好
// 在本例中,ch2 会在 1 秒后先就绪
select {
case msg1 := <-ch1:
fmt.Println("收到了:", msg1)
case msg2 := <-ch2:
fmt.Println("收到了:", msg2) // 这条 case 会被执行
}
fmt.Println("第一次 select 结束。")
// [补充示例] 带 default 的非阻塞 select
// 此时 ch1 和 ch2 都为空,也没有 goroutine 会向它们发送数据
select {
case msg1 := <-ch1:
fmt.Println("第二次收到了:", msg1)
case msg2 := <-ch2:
fmt.Println("第二次收到了:", msg2)
default:
// 因为 ch1 和 ch2 都不可读,所以会立即执行 default
fmt.Println("没有任何消息可以立即接收。")
}
}
```
# 7. 应用场景:定时器
`time` 包中的定时器功能是 Channel 的一个极佳应用范例。
- **`time.NewTimer`**: 创建一个定时器对象,在指定时间后,会将当前时间发送到其内部的 `C` Channel 中。可以调用 `Stop()` 来取消。
- **`time.After`**: 一个更简洁的用法,返回一个 Channel。在指定时间后,该 Channel 会接收到一个时间值。它像一个一次性的闹钟。
- **`time.AfterFunc`**: 在指定时间后,直接执行一个函数,不使用 Channel。
```go
package main
import (
"fmt"
"time"
)
func sendEmail() {
fmt.Println("定时任务触发:正在发送邮件...")
}
func main() {
// 示例 1: 使用 time.After 等待一个事件
fmt.Println("程序启动时间:", time.Now().Format("15:04:05"))
fmt.Println("设置一个 2 秒后触发的事件...")
// time.After 返回一个 <-chan Time 类型的通道
// main goroutine 会阻塞在这里,直到 2 秒后通道接收到值
timerChan := time.After(2 * time.Second)
<-timerChan
fmt.Println("2 秒已到,事件触发!当前时间:", time.Now().Format("15:04:05"))
// 示例 2: 使用 time.AfterFunc 安排一个未来的回调函数
// 这是一种 "fire and forget" 的方式,不会阻塞当前 goroutine
fmt.Println("\n设置一个 3 秒后执行的邮件发送任务...")
time.AfterFunc(3*time.Second, sendEmail)
// 主程序需要等待足够长的时间,以确保 AfterFunc 的任务有机会执行
// 在实际应用中,主程序通常有自己的主循环,不会这样简单地退出
fmt.Println("主程序继续执行其他任务...")
time.Sleep(4 * time.Second) // 等待超过 3 秒
fmt.Println("\n主程序退出。")
}
```
---
# 8. 拓展学习
- **Go 并发模型 (GPM)**: 要深入理解 Goroutine 是如何被 Go 运行时调度的,可以学习 GPMGoroutine, Processor, Machine)模型。您提供的链接是一个很好的起点:[GPM模型讲解](https://www.bilibili.com/video/BV1hv411x7we?p=16)
+351
View File
@@ -0,0 +1,351 @@
# 一、核心概念:进程、线程与协程
- **进程 (Process)**
- **定义**: 程序的一次执行过程,是操作系统进行**资源分配**的基本单位。
- **特点**: 拥有独立的内存空间,创建和切换开销巨大,资源占用多。
- **线程 (Thread)**
- **定义**: 进程内的执行单元,是 **CPU 调度**的基本单位。一个进程至少包含一个主线程。
- **特点**: 共享进程的内存空间,创建和切换开销相比进程小,但仍有相当成本。多线程共享数据时需要处理同步问题(如锁)。
- **协程 (Coroutine / Goroutine)**
- **定义**: Go 语言实现的特有并发体,是一种**用户态的、极其轻量级的线程**。
- **特点**:
- **开销极小**: 创建成本远低于线程,内存占用仅为KB级别(初始约2KB),可以轻松创建数十万甚至上百万个。
- **调度灵活**: 由 Go 语言的运行时(Runtime)进行调度,而非操作系统。调度器能够在 I/O 操作等场景下自动切换,实现高效的并发。
- **非抢占式**: 协程出让 CPU 控制权通常发生在特定的时机(如Channel操作、系统调用、函数调用等),而不是由操作系统强制中断。
## 对比总结
| 特性 | 进程 (Process) | 线程 (Thread) | **协程 (Goroutine)** |
| :------- | :------------- | :----------------- | :------------------------ |
| **角色** | 资源分配单位 | CPU 调度单位 | **Go语言的并发执行体** |
| **开销** | 巨大 | 较大 | **极小** |
| **数量级** | 百级 | 万级 | **百万级** |
| **调度方** | 操作系统 | 操作系统 | **Go 运行时 (Runtime)** |
| **内存** | 独立 | 共享进程内存 | **共享进程内存** |
---
# 二、GoroutineGo 并发编程的基石
Go 语言通过 `go` 关键字来创建 Goroutine,实现并发执行。一个 Goroutine 就是一个与其他函数或方法同时运行的函数。
## 1. 启动一个 Goroutine
语法非常简单,在函数或方法调用前加上 `go` 关键字即可。
```go
go myFunction() // 启动一个新的 Goroutine 来执行 myFunction
```
## 2. Goroutine 的执行特性
1. **立即返回**: `go` 语句会立即返回,不会等待新的 Goroutine 执行完毕。
2. **主 Goroutine 退出**: `main` 函数本身运行在一个特殊的 Goroutine 中,称为主 Goroutine。**如果主 Goroutine 退出,整个程序就会结束,所有其他正在运行的 Goroutine 也会被强制终止。**
### 示例:主 Goroutine 提前退出
下面的代码中,`hello()` 可能根本不会打印任何内容,因为主 Goroutine 在它开始执行前就已经结束了。
```go
package main
import (
"fmt"
"time"
)
func hello() {
fmt.Println("Hello from new Goroutine!")
}
func main() {
// 启动一个新的 Goroutine
go hello()
fmt.Println("main function finished.")
// 如果没有下面的休眠,程序会立刻退出,"Hello from new Goroutine!" 很可能没有机会打印
// 这是一种不推荐的做法,仅用于演示。正确的方式是使用 sync.WaitGroup。
time.Sleep(1 * time.Second)
}
```
## 3. `sync.WaitGroup`:等待 Goroutine 完成
为了解决主 Goroutine 提前退出的问题,我们需要一种机制来等待其他 Goroutine 执行完毕。`sync.WaitGroup` 是最常用的工具。
- `wg.Add(n)`:增加计数器,表示需要等待 n 个 Goroutine。
- `wg.Done()`:当一个 Goroutine 完成任务时调用,使计数器减一。通常使用 `defer wg.Done()` 来确保执行。
- `wg.Wait()`:阻塞主 Goroutine,直到计数器归零。
### 规范化代码示例:
```go
package main
import (
"fmt"
"sync"
"time"
)
// 定义一个 WaitGroup
var wg sync.WaitGroup
func worker(id int) {
// 在函数退出时,通知 WaitGroup 任务已完成
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
// 模拟耗时操作
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
// 启动 3 个 Goroutine
for i := 1; i <= 3; i++ {
// 每启动一个 Goroutine,计数器加 1
wg.Add(1)
go worker(i)
}
fmt.Println("Main: Waiting for workers to finish...")
// 等待所有 Goroutine 完成(即等待计数器归零)
wg.Wait()
fmt.Println("Main: All workers have finished. Exiting.")
}
```
---
# 三、并发中的资源竞争与同步
当多个 Goroutine 同时访问和修改同一个共享资源(如一个变量)时,就会产生**资源竞争 (Race Condition)**,这会导致程序结果不可预测,是并发编程中的常见陷阱。
## 1. 资源竞争问题演示(售票)
下面的代码模拟了多个售票窗口同时售票的场景,由于没有同步机制,会导致超卖或数据不一致。
### 规范化代码示例:
```go
package main
import (
"fmt"
"sync"
"time"
)
var ticket = 10 // 共享资源:总票数
func saleTickets(name string, wg *sync.WaitGroup) {
defer wg.Done()
for {
// 在检查和修改票数之间,可能会发生上下文切换,导致竞争
if ticket > 0 {
// 模拟处理耗时,增加竞争发生概率
time.Sleep(10 * time.Millisecond)
fmt.Printf("%s 售出 1 张票, 剩余 %d 张\n", name, ticket-1)
ticket--
} else {
fmt.Printf("%s 发现票已售罄\n", name)
break
}
}
}
func main() {
var wg sync.WaitGroup
// 启动 4 个售票窗口
wg.Add(4)
go saleTickets("窗口1", &wg)
go saleTickets("窗口2", &wg)
go saleTickets("窗口3", &wg)
go saleTickets("窗口4", &wg)
wg.Wait()
fmt.Println("所有售票窗口关闭。")
}
```
运行上述代码,你会看到类似 `剩余 -1 张` 的错误结果,这就是资源竞争的后果。
## 2. 解决方案一:`sync.Mutex` 互斥锁
互斥锁(Mutex)是一种保护**临界区(Critical Section)**的常用工具,它确保同一时间只有一个 Goroutine 可以访问被保护的共享资源。
- `mutex.Lock()`: 获取锁。如果锁已被其他 Goroutine 持有,则当前 Goroutine 会阻塞,直到可以获取锁。
- `mutex.Unlock()`: 释放锁,允许其他等待的 Goroutine 获取。
### 规范化代码示例(修复后的售票问题):
```go
package main
import (
"fmt"
"sync"
"time"
)
var ticket = 10 // 共享资源
var mutex sync.Mutex // 互斥锁
var wg sync.WaitGroup
func saleTicketsWithLock(name string) {
defer wg.Done()
for {
// 在访问共享资源前加锁
mutex.Lock()
if ticket > 0 {
time.Sleep(10 * time.Millisecond)
fmt.Printf("%s 售出 1 张票, 剩余 %d 张\n", name, ticket-1)
ticket--
// 操作完成后解锁
mutex.Unlock()
} else {
// 如果票已售完,也要解锁,否则会造成死锁,其他Goroutine永远无法进入
mutex.Unlock()
fmt.Printf("%s 发现票已售罄\n", name)
break
}
}
}
func main() {
// 启动 4 个售票窗口
wg.Add(4)
go saleTicketsWithLock("窗口1")
go saleTicketsWithLock("窗口2")
go saleTicketsWithLock("窗口3")
go saleTicketsWithLock("窗口4")
wg.Wait()
fmt.Println("所有售票窗口关闭。")
}
```
## 3. Go 的哲学:通过通信共享内存
您笔记中提到了一句非常重要的话:**"不要以共享内存的方式去通信,而要以通信的方式去共享内存。"** 这是 Go 并发设计的核心哲学。
- **共享内存通信**: 使用锁 (`sync.Mutex`) 保护共享变量,是传统的多线程编程模型。
- **通信共享内存**: 使用 **Channel** 在 Goroutine 之间传递数据(或数据的所有权)。数据在同一时间只被一个 Goroutine 持有,从而天然地避免了资源竞争。这是 Go 更推崇的方式。
### 示例:使用 Channel 控制并发任务
虽然 Channel 也能解决售票问题(实现起来更复杂),但它更常用于生产者-消费者模型。下面是一个简单的例子,展示其基本用法:
```go
package main
import (
"fmt"
"time"
)
// jobs channel 用于发送工作,results channel 用于接收结果
func worker(id int, jobs <-chan int, results chan<- int) {
// 从 jobs channel 接收任务
for j := range jobs {
fmt.Printf("Worker %d started job %d\n", id, j)
time.Sleep(time.Second) // 模拟耗时
fmt.Printf("Worker %d finished job %d\n", id, j)
// 将结果发送到 results channel
results <- j * 2
}
}
func main() {
const numJobs = 5
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
// 启动 3 个 worker Goroutine
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 发送 5 个任务到 jobs channel
for j := 1; j <= numJobs; j++ {
jobs <- j
}
// 关闭 jobs channel,表示没有更多任务了
// worker 的 for-range 循环会因此而结束
close(jobs)
// 从 results channel 收集所有结果
for a := 1; a <= numJobs; a++ {
<-results
}
fmt.Println("All jobs are done.")
}
```
---
# 四、`runtime` 包:与 Go 调度器交互
`runtime` 包提供了与 Go 运行时系统交互的能力,例如控制调度器或获取系统信息。
- `runtime.NumCPU()`: 返回当前系统的 CPU 核心数。
- `runtime.GOMAXPROCS(n)`: **[补充知识点]** 设置可同时执行 Go 代码的 CPU 最大数量。自 Go 1.5 起,默认值就是 `runtime.NumCPU()`,通常无需手动设置。
- `runtime.Gosched()`: 让出当前 Goroutine 的执行权限,让调度器去执行其他 Goroutine。当前 Goroutine 会被放回队列,未来会恢复执行。
- `runtime.Goexit()`: 立即终止当前的 Goroutine,但不会影响其他 Goroutine。`defer` 语句在该 Goroutine 中仍会被执行。
### 规范化代码示例:
```go
package main
import (
"fmt"
"runtime"
"sync"
)
func exitDemo() {
defer fmt.Println("Goroutine A: defer is executed")
fmt.Println("Goroutine A: about to exit.")
runtime.Goexit() // 终止当前 Goroutine
fmt.Println("Goroutine A: this line will never be reached.")
}
func main() {
var wg sync.WaitGroup
// --- NumCPU ---
fmt.Printf("System CPU count: %d\n", runtime.NumCPU())
// --- Goexit demo ---
wg.Add(1)
go func() {
defer wg.Done()
exitDemo()
}()
// --- Gosched demo ---
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 2; i++ {
fmt.Println("Goroutine B:", i)
// 让出 CPU,让 main Goroutine 有机会执行
runtime.Gosched()
}
}()
// Main Goroutine 的工作
for i := 0; i < 2; i++ {
fmt.Println("Main Goroutine:", i)
runtime.Gosched()
}
wg.Wait()
fmt.Println("Main Goroutine finished.")
}
```
+703
View File
@@ -0,0 +1,703 @@
# 1️⃣ 函数基础
## 1.1 函数定义与语法
```go
package main
import (
"errors"
"fmt"
)
// 函数的基本定义格式
func main() {
// 简单函数调用示例
fmt.Println("无返回值函数:")
sayHello()
fmt.Println("\n有参数有返回值函数:")
fmt.Println("5 + 3 =", add(5, 3))
fmt.Println("\n多返回值函数:")
result, err := divide(10, 2)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("10 / 2 =", result)
}
fmt.Println("\n命名返回值函数:")
area, perimeter := rectangle(5, 3)
fmt.Printf("面积: %.2f, 周长: %.2f\n", area, perimeter)
fmt.Println("\n可变参数函数:")
fmt.Println("sum(1, 2, 3) =", sum(1, 2, 3))
numbers := []int{1, 2, 3, 4, 5}
fmt.Println("sum(numbers...) =", sum(numbers...))
}
// 无参数无返回值函数
func sayHello() {
fmt.Println("Hello")
}
// 带参数和返回值的函数
// 多个相同类型的参数可以简写(x, y int)
func add(x, y int) int {
return x + y
}
// 多返回值函数(典型的 Go 错误处理模式)
func divide(x, y float64) (float64, error) {
if y == 0 {
return 0, errors.New("除数不能为0")
}
return x / y, nil
}
// 命名返回值函数
// 可以直接使用空 return 返回命名变量
func rectangle(width, height float64) (area, perimeter float64) {
area = width * height
perimeter = 2 * (width + height)
return // 直接返回命名变量
}
// 可变参数函数
// ...int 表示接受任意数量的 int 参数
func sum(nums ...int) int {
total := 0
// nums 在函数内部是一个 []int 切片
for _, num := range nums {
total += num
}
return total
}
```
## 1.2 参数传递机制(关键澄清)
Go 语言**只有值传递**这一种参数传递方式!这是一个常见的误解。即使是 slice、map、channel 等"引用类型",也是通过值传递的,只是它们的值本身包含了指针信息。
```go
package main
import "fmt"
func main() {
// 值类型示例
fmt.Println("值类型传递示例:")
a := 10
fmt.Println("调用前 a =", a)
modifyInt(a)
fmt.Println("调用后 a =", a)
// 指针类型示例
fmt.Println("\n指针值传递示例:")
b := 20
fmt.Println("调用前 b =", b)
modifyIntPtr(&b)
fmt.Println("调用后 b =", b)
// 引用类型(内部有指针)示例
fmt.Println("\n引用类型值传递示例:")
c := make([]int, 3)
fmt.Printf("调用前 c = %#v, 地址 = %p\n", c, &c[0])
modifySlice(c)
fmt.Printf("调用后 c = %#v, 地址 = %p\n", c, &c[0])
// 深度修改引用类型的示例
fmt.Println("\n切片重新分配内存示例:")
d := []int{1, 2, 3}
fmt.Println("调用前 d =", d)
reallocSlice(d)
fmt.Println("调用后 d =", d)
}
// ❌ 值传递:修改的是副本,不影响原始值
func modifyInt(a int) {
a = 100
fmt.Println("modifyInt 内 a =", a)
}
// ✅ 通过指针值传递:修改指针指向的内容,影响原始值
func modifyIntPtr(a *int) {
*a = 200
fmt.Println("modifyIntPtr 内 *a =", *a)
}
// ✅ 修改引用类型的内容(不影响切片头本身)
func modifySlice(s []int) {
s[0] = 300
fmt.Printf("modifySlice 内 s = %#v, 地址 = %p\n", s, &s[0])
}
// ❌ 尝试重新分配内存不会影响调用方
func reallocSlice(s []int) {
s = append(s, 4)
s[0] = 400
fmt.Println("reallocSlice 内 s =", s)
}
```
**关键总结**
- Go **只有值传递**,不存在引用传递
- 对于基本类型(int, string, struct等),传递的是数据副本
- 对于引用类型(slice, map, channel等),传递的是包含指针信息的结构体副本
- 要修改调用方的数据,需要使用指针传递
## 1.3 作用域规则
```go
package main
import "fmt"
// 全局变量(包级作用域)
var globalVar = "全局变量"
// 标准化后的变量命名示例
var (
DebugMode = false
MaxRetries = 3
)
func main() {
fmt.Println("=== Go 作用域规则演示 ===")
// 局部变量(函数级作用域)
localVar := "局部变量"
fmt.Println("main 函数内:", globalVar, localVar)
// 块级作用域示例
if score := 90; score >= 60 {
// score 只在 if 块内可见
fmt.Printf("成绩为 %d: 及格!\n", score)
// 可以访问外层变量
fmt.Println("可以访问全局变量:", globalVar)
}
// 无法访问 score,会编译错误
// fmt.Println(score)
// 变量遮蔽演示
name := "外层变量"
{
name := "内层变量" // 遮蔽了外层变量
fmt.Println("块内:", name)
}
fmt.Println("块外:", name)
shadowingExample()
}
// 变量遮蔽的另一个示例
func shadowingExample() {
message := "原始消息"
if message := "块级消息"; true {
fmt.Println("if 块内:", message) // 输出: 块级消息
}
fmt.Println("函数内:", message) // 输出: 原始消息
}
```
1. **最小作用域原则**:将变量定义在最小必要作用域内
2. **避免全局状态**:尽可能使用局部变量替代全局变量
3. **明确的变量命名**:变量名应清晰表达用途,特别注意避免遮蔽
4. **包级变量初始化**:使用 `var ()` 块集中声明相关全局变量
5. **常量使用**:使用 const 声明不会改变的值,提高代码可读性
---
# 2️⃣ 函数高级特性
## 2.1 函数作为一等公民
Go 中函数是**一等公民**(first-class citizen),具有以下特性:
```go
package main
import "fmt"
func main() {
fmt.Println("\n=== 函数作为一等公民 ===")
// 1. 函数赋值给变量
sumFunc := func(a, b int) int {
return a + b
}
fmt.Println("函数变量 sum(5, 3):", sumFunc(5, 3))
// 2. 作为参数传递
fmt.Println("操作结果:", operate(5, 3, sumFunc))
multiply := func(a, b int) int { return a * b }
fmt.Println("操作结果:", operate(5, 3, multiply))
// 3. 作为返回值
adder := createAdder(10)
fmt.Println("adder(5):", adder(5)) // 输出: 15
// 4. 匿名函数直接调用
func(message string) {
fmt.Println("匿名函数直接执行:", message)
}("Hello World")
}
// 函数作为参数
func operate(a, b int, op func(int, int) int) int {
return op(a, b)
}
// 函数作为返回值(闭包)
func createAdder(x int) func(int) int {
return func(y int) int {
return x + y
}
}
```
## 2.2 闭包详解
闭包(closure)是指能够访问并记住其创建环境的匿名函数。Go 闭包的**核心机制**是函数值与相关变量的绑定。
```go
package main
import "fmt"
func main() {
fmt.Println("\n=== 闭包深入解析 ===")
// 基本闭包示例
counter := createCounter()
fmt.Println("counter():", counter()) // 1
fmt.Println("counter():", counter()) // 2
// 闭包共享变量示例
nextID, reset := createUserSession()
fmt.Println("用户ID:", nextID()) // 1001
fmt.Println("用户ID:", nextID()) // 1002
reset()
fmt.Println("重置后用户ID:", nextID()) // 1001
// 闭包与循环问题
fmt.Println("\n循环中创建闭包问题修复:")
f := createFunctions()
for i, fn := range f {
fmt.Printf("函数 %d 执行结果: %d\n", i, fn())
}
}
// 基础计数器闭包
func createCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
// 复杂闭包示例:共享状态
func createUserSession() (nextID func() int, reset func()) {
currentID := 1000
nextID = func() int {
currentID++
return currentID
}
reset = func() {
currentID = 1000
}
return nextID, reset
}
// [关键] 循环中闭包问题的正确处理
func createFunctions() []func() int {
functions := make([]func() int, 3)
// 方案1:在循环内创建新变量
for i := range functions {
i := i // 创建新的i变量绑定到每个闭包
functions[i] = func() int {
return i * 10
}
}
// 方案2:在循环中创建函数
// for i := range functions {
// functions[i] = func(idx int) func() int {
// return func() int { return idx * 10 }
// }(i)
// }
return functions
}
```
**闭包原理**
1. 闭包不是即时执行的,而是**捕获创建时的变量环境**
2. 闭包中的变量会持续存在,直到没有引用它们的函数
3. 多个闭包可以共享相同的变量环境
4. 在循环中直接使用索引变量会导致所有闭包捕获相同的最终值(常见陷阱)
## 2.3 defer 机制深度解析
```go
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("\n=== defer 机制详解 ===")
// 1. 基本执行顺序 (LIFO)
illustrateDeferOrder()
// 2. 参数求值时机
illustrateDeferEvaluation()
// 3. 实际应用场景:资源清理
handleFile()
// 4. defer + 错误处理模式
if err := processResource(); err != nil {
fmt.Println("处理过程中出现错误:", err)
}
}
// 演示 defer 的执行顺序 (后进先出)
func illustrateDeferOrder() {
fmt.Println("\n--- defer 执行顺序 (LIFO) ---")
defer fmt.Println("defer 1: 最后执行")
defer fmt.Println("defer 2: 倒数第二执行")
defer fmt.Println("defer 3: 倒数第三执行")
fmt.Println("立即执行: 主函数主体")
}
// 演示 defer 参数求值时机
func illustrateDeferEvaluation() {
fmt.Println("\n--- defer 参数求值时机 ---")
i := 1
// defer 语句执行时,参数就已经求值
defer fmt.Println("直接值传递(执行时):", i) // 捕获当前 i 值 (1)
// 闭包捕获:捕获的是变量引用
defer func() {
fmt.Println("闭包捕获(返回时):", i)
}() // i 在此时尚未改变
i = 2
// 参数传递方式:参数在 defer 语句执行时求值
defer func(j int) {
fmt.Println("参数传递方式:", j) // j 是 2,因为 defer 执行时 i 是 2
}(i)
i = 3
}
// 实际应用场景:文件处理
func handleFile() {
fmt.Println("\n--- 文件处理 with defer ---")
filename := "example.txt"
// 创建示例文件
file, err := os.Create(filename)
if err != nil {
fmt.Println("创建文件失败:", err)
return
}
// 确保文件被关闭
defer func() {
fmt.Println("关闭文件...")
if err := file.Close(); err != nil {
fmt.Println("关闭文件时出错:", err)
}
}()
// 写入内容
if _, err := file.WriteString("Hello, defer!"); err != nil {
fmt.Println("写入文件失败:", err)
return
}
fmt.Println("文件写入成功")
}
// defer + 错误处理最佳实践
func processResource() error {
fmt.Println("\n--- defer + 错误处理最佳实践 ---")
resource, err := acquireResource()
if err != nil {
return fmt.Errorf("获取资源失败: %w", err)
}
// 确保资源被释放,即使后面发生错误
defer func() {
fmt.Println("释放资源...")
releaseResource(resource)
}()
// 模拟业务逻辑
fmt.Println("处理资源中...")
if err := performOperation(resource); err != nil {
return fmt.Errorf("操作失败: %w", err)
}
// 操作成功
fmt.Println("操作成功完成!")
return nil
}
// 模拟资源操作
func acquireResource() (string, error) {
fmt.Println("获取资源...")
return "resource_data", nil
}
func releaseResource(r string) {
fmt.Printf("资源 %q 已释放\n", r)
}
func performOperation(r string) error {
// 模拟成功操作
return nil
// return errors.New("模拟操作失败")
}
```
1. **执行时机**:在包含它的函数执行 `return` 语句或发生 panic 后、函数真正返回前执行
2. **参数求值**:参数在 `defer` 语句执行时求值,函数在 deferred 时执行
3. **执行顺序**:LIFO(后进先出),与声明顺序相反
4. **与 panic 的关系**:无论是正常返回还是由于 panic 返回,defer 都会执行
5. **作用域**:在函数体的任何位置声明的 defer 都会在函数返回前执行
**defer 常见陷阱**
```go
// 陷阱1:在循环中使用 defer 可能导致资源延迟释放
func badLoopDefer() {
for i := 0; i < 10; i++ {
file, _ := os.Open(fmt.Sprintf("file_%d.txt", i))
defer file.Close() // 所有关闭操作都延迟到最后
}
// 其他代码执行时已打开10个文件,但未关闭
}
// 正确做法:使用内部函数
func goodLoopDefer() {
for i := 0; i < 10; i++ {
func(i int) {
file, _ := os.Open(fmt.Sprintf("file_%d.txt", i))
defer file.Close()
// 处理文件
}(i)
}
}
// 陷阱2nil 接收者会导致 panic
func badHTTPDefer() {
resp, err := http.Get("https://example.com")
if err != nil {
// 此处 resp 为 nil
defer resp.Body.Close() // 会导致 panic
return
}
// ...
}
// 正确做法:先检查 nil 再 defer
func goodHTTPDefer() {
resp, err := http.Get("https://example.com")
if err != nil {
return err
}
defer resp.Body.Close() // 此时 resp 不为 nil
// ...
}
```
## 2.4 方法值与方法表达式
```go
package main
import "fmt"
// 定义一个简单的类型
type Dog struct {
name string
}
// 为 Dog 类型定义方法
func (d Dog) Bark(volume int) {
vol := ""
for i := 0; i < volume; i++ {
vol += "!"
}
fmt.Printf("%s: 汪%s\n", d.name, vol)
}
// 方法表达式示例
func (d Dog) WithPrefix(prefix string) string {
return fmt.Sprintf("[%s] %s", prefix, d.name)
}
func main() {
fmt.Println("\n=== 方法值与方法表达式 ===")
dog := Dog{name: "小黑"}
// 1. 方法值:绑定接收者
bark := dog.Bark
bark(1) // 小黑: 汪!
bark(3) // 小黑: 汪!!!
// 2. 方法表达式:不绑定接收者
generalBark := Dog.Bark
generalBark(dog, 2) // 小黑: 汪!!
// 方法表达式在函数参数中的应用
processDog(dog, Dog.WithPrefix)
}
// 使用方法表达式作为参数
func processDog(d Dog, formatter func(Dog, string) string) {
fmt.Println("处理后的名字:", formatter(d, "宠物"))
}
```
---
## 3️⃣ 递归与特殊函数模式
### 3.1 递归函数
```go
package main
import (
"fmt"
)
func main() {
fmt.Println("\n=== 递归函数详解 ===")
// 基础递归示例
fmt.Println("sumRecursive(5) =", sumRecursive(5))
// 递归深度问题
showRecursionLimit()
// 递归优化示例
fmt.Println("\n斐波那契数列:")
fmt.Println("fibBad(10) =", fibBad(10)) // 低效递归
fmt.Println("fibGood(10) =", fibGood(10)) // 优化版本
}
// 递归求和函数
func sumRecursive(n int) int {
// 1. 终止条件
if n <= 1 {
return n
}
// 2. 递归调用
return n + sumRecursive(n-1)
}
// 递归深度限制示例
func showRecursionLimit() {
fmt.Println("\n递归深度限制演示:")
defer func() {
if r := recover(); r != nil {
fmt.Println("递归深度过大导致栈溢出:", r)
}
}()
// 尝试过深的递归
var deepRecursion func(int)
deepRecursion = func(n int) {
if n%1000 == 0 {
fmt.Printf("递归深度: %d\n", n)
}
deepRecursion(n + 1)
}
deepRecursion(0)
}
// 低效的斐波那契递归(存在大量重复计算)
func fibBad(n int) int {
if n <= 1 {
return n
}
return fibBad(n-1) + fibBad(n-2)
}
// 优化的斐波那契(迭代方法)
func fibGood(n int) int {
if n <= 1 {
return n
}
a, b := 0, 1
for i := 2; i <= n; i++ {
a, b = b, a+b
}
return b
}
```
1. **必须有明确的终止条件** - 无限递归会导致栈溢出
2. **递归规模应逐步缩小** - 典型模式:处理部分数据后递归剩余数据
3. **避免重复计算** - 使用记忆化技术(memoization)存储已计算结果
4. **考虑替代方案** - 对于可能深度递归的场景,优先考虑迭代实现
5. **注意栈大小限制** - Go 默认栈大小约 1-8MB,可使用 `debug.SetMaxStack` 调整
记忆化递归优化示例:
```go
package main
import "fmt"
func main() {
fmt.Println("\n记忆化斐波那契递归:")
cache := make(map[int]int)
fmt.Println("fibMemo(10) =", fibMemo(10, cache))
}
// 记忆化斐波那契递归
func fibMemo(n int, cache map[int]int) int {
// 检查是否已计算
if val, found := cache[n]; found {
return val
}
// 基础情况
if n <= 1 {
cache[n] = n
return n
}
// 递归计算并保存结果
cache[n] = fibMemo(n-1, cache) + fibMemo(n-2, cache)
return cache[n]
}
```
@@ -0,0 +1,465 @@
# Go 语言切片详解:从基础到实践
在 Go 语言中,**切片(Slice)** 是对数组一个连续片段的引用。它是 Go 中最核心、最灵活的数据结构,几乎在所有场景下都取代了数组。理解切片是掌握 Go 语言的关键一步,它深刻影响着程序的性能和内存管理。
---
## 1. 切片基础概念
切片是一个**封装了底层数组信息的结构体**,它本身不存储任何数据,只是一个“视图”或“窗口”。
### 核心特征
-**类型一致性**:所有元素必须是相同类型。
-**动态长度**:长度可变,可以随时通过 `append` 等操作增删元素。
-**引用类型**:赋值和传参时,传递的是切片头的**浅拷贝**,但它们指向**同一个底层数组**。修改其中一个切片的元素会影响到另一个。
-**包含三个核心组件**
1. **指针 (Pointer)**:指向底层数组中切片指定开始位置的内存地址。
2. **长度 (Length)**:切片中元素的数量,`len()` 函数获取。
3. **容量 (Capacity)**:从切片开始位置到底层数组末尾的元素数量,`cap()` 函数获取。
> ⚠️ 注意:切片是否拥有自己的底层数组是不确定的。它可能与其它切片共享,也可能在扩容后获得一个新的底层数组。
---
## 2. 切片声明与初始化
### 示例 1:基本声明与初始化
```go
package main
import "fmt"
func main() {
// 方式一:使用字面量直接初始化
s1 := []int{1, 2, 3, 4, 5}
fmt.Printf("s1: %v, len: %d, cap: %d\n", s1, len(s1), cap(s1))
// s1: [1 2 3 4 5], len: 5, cap: 5
// 方式二:使用 make 函数创建,可以指定长度和容量
// make([]T, length, capacity)
s2 := make([]int, 5, 10)
fmt.Printf("s2: %v, len: %d, cap: %d\n", s2, len(s2), cap(s2))
// s2: [0 0 0 0 0], len: 5, cap: 10
// 方式三:声明一个 nil 切片
var s3 []int
fmt.Printf("s3: %v, len: %d, cap: %d\n", s3, len(s3), cap(s3))
fmt.Println("s3 is nil?", s3 == nil) // true
// s3: [], len: 0, cap: 0
// s3 is nil? true
}
```
> ✅ **nil 切片** vs **空切片**`var s []int` 是 nil 切片,`s := []int{}` 或 `s := make([]int, 0)` 是空切片。它们 `len` 和 `cap` 都为 0,但 `nil` 切片不指向任何底层数组。在实践中,`append`、`len`、`cap` 和 `range` 对它们的操作效果完全一样。
---
### 示例 2:从数组或切片创建(切片表达式)
这是创建切片最常见的方式,`array[low:high:max]`
- `low`:开始索引(包含)
- `high`:结束索引(不包含)
- `max`:设置容量(可选),`cap = max - low`
```go
package main
import "fmt"
func main() {
arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// s[low:high] -> len=high-low, cap=cap(arr)-low
s1 := arr[2:5]
fmt.Printf("s1: %v, len: %d, cap: %d\n", s1, len(s1), cap(s1))
// s1: [2 3 4], len: 3, cap: 8
// 省略 low,默认为 0
s2 := arr[:5]
fmt.Printf("s2: %v, len: %d, cap: %d\n", s2, len(s2), cap(s2))
// s2: [0 1 2 3 4], len: 5, cap: 10
// 省略 high,默认为 len(arr)
s3 := arr[5:]
fmt.Printf("s3: %v, len: %d, cap: %d\n", s3, len(s3), cap(s3))
// s3: [5 6 7 8 9], len: 5, cap: 5
// s[low:high:max] -> len=high-low, cap=max-low
s4 := arr[2:5:7] // 从 arr[2] 开始,长度为3,容量限制到 arr[7]
fmt.Printf("s4: %v, len: %d, cap: %d\n", s4, len(s4), cap(s4))
// s4: [2 3 4], len: 3, cap: 5
}
```
---
## 3. 切片操作详解
### 示例 3:访问、修改和遍历
切片的访问和遍历方式与数组完全相同。
```go
package main
import "fmt"
func main() {
s := []string{"Go", "Java", "Python", "Rust"}
// 访问
fmt.Println("第一个元素:", s[0]) // Go
// 修改
s[1] = "C++"
fmt.Println("修改后:", s) // [Go C++ Python Rust]
// 遍历(与数组的三种方式一致)
for i, v := range s {
fmt.Printf("索引 %d -> 值 %s\n", i, v)
}
}
```
> ⚠️ 越界访问 `s[i]` 同样会导致 `panic`。
### 示例 4:追加元素 `append`(核心操作)
`append` 是切片最重要的函数,用于向切片末尾添加元素。
```go
package main
import "fmt"
func main() {
s := make([]int, 0, 3) // len=0, cap=3
fmt.Printf("初始 -> len: %d, cap: %d, ptr: %p\n", len(s), cap(s), s)
// 1. 容量足够,不发生扩容
s = append(s, 1)
s = append(s, 2)
fmt.Printf("追加2个后 -> len: %d, cap: %d, ptr: %p\n", len(s), cap(s), s)
// 2. 容量不足,发生扩容
// Go 会分配一个更大的新数组,将旧数据复制过去,再添加新元素
s = append(s, 3) // 容量刚好用完
s = append(s, 4) // 触发扩容
fmt.Printf("触发扩容后 -> len: %d, cap: %d, ptr: %p\n", len(s), cap(s), s)
// 扩容策略:通常是翻倍(当元素较少时)或乘以1.25(当元素较多时)
// 输出中可以看到 ptr 地址发生了变化
// 追加另一个切片(使用 ...
s = append(s, []int{5, 6, 7}...)
fmt.Printf("追加切片后 -> len: %d, cap: %d\n", len(s), cap(s))
}
```
### 示例 5:复制切片 `copy`
`copy(dst, src)` 函数用于将 `src` 切片中的元素复制到 `dst` 切片。它只复制**两者长度的最小值**个元素。
```go
package main
import "fmt"
func main() {
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src)
fmt.Println("复制的元素个数:", n) // 3
fmt.Println("目标切片 dst:", dst) // [1 2 3]
fmt.Println("源切片 src:", src) // [1 2 3 4 5] - 不受影响
// copy 创建了独立的副本,修改 dst 不会影响 src
dst[0] = 99
fmt.Println("修改后 dst:", dst) // [99 2 3]
fmt.Println("修改后 src:", src) // [1 2 3 4 5]
}
```
> ✅ `copy` 是避免切片共享底层数组副作用的有效手段。
---
## 4. 多维切片详解
多维切片是“切片的切片”,与多维数组不同,其内部的切片长度可以**不一致**。
```go
package main
import "fmt"
func main() {
// 创建一个 "锯齿" 切片
matrix := [][]int{
{1, 2},
{3, 4, 5, 6},
{7},
}
fmt.Println("多维切片内容:", matrix)
matrix[0][1] = 99 // 修改元素
fmt.Println("修改后:", matrix)
// 遍历
for i := range matrix {
fmt.Println("Row", i, ":", matrix[i])
}
}
```
---
## 5. 切片作为引用类型的特性
### 示例 6:赋值和传参共享底层数组
```go
package main
import "fmt"
func modifySlice(s []string) {
s[0] = "MODIFIED"
fmt.Println("函数内 s:", s)
}
func main() {
original := []string{"a", "b", "c"}
fmt.Println("原始切片:", original)
// 赋值是浅拷贝
refCopy := original
refCopy[1] = "CHANGED"
fmt.Println("赋值后 original:", original) // 受影响
fmt.Println("赋值后 refCopy:", refCopy)
// 函数传参也是浅拷贝
modifySlice(original)
fmt.Println("函数外 original:", original) // 再次受影响
}
```
> 输出:
> 原始切片: [a b c]
> 赋值后 original: [a CHANGED c]
> 赋值后 refCopy: [a CHANGED c]
> 函数内 s: [MODIFIED CHANGED c]
> 函数外 original: [MODIFIED CHANGED c]
---
## 6. 切片的常见陷阱与注意事项
### ❌ 陷阱 1`append` 可能改变原有切片
当函数内部的 `append` **没有**触发扩容时,修改会反映到外部;一旦**触发扩容**(返回了新的底层数组),修改就不会反映到外部。
```go
package main
import "fmt"
func appendTrap(s []int) {
// 此时 s 的 len=3, cap=5。append 不会触发扩容
s = append(s, 100)
fmt.Println("函数内 s:", s) // [0 1 2 100]
}
func main() {
original := make([]int, 3, 5) // [0,0,0], len=3, cap=5
fmt.Println("调用前 original:", original) // [0 0 0]
appendTrap(original)
// original 的长度没变,但底层数组的数据被修改了!
fmt.Println("调用后 original:", original) // [0 0 0] - len 没变
// 查看底层数组的变化
fmt.Println("底层数组情况:", original[:cap(original)]) // [0 0 0 100 0]
}
```
**正确做法**:函数如果修改了切片(特别是通过 `append`),**永远应该返回新的切片**。
```go
func appendSafe(s []int) []int {
return append(s, 100)
}
// 调用: original = appendSafe(original)
```
---
### ❌ 陷阱 2:子切片可能导致内存泄漏
如果你从一个非常大的切片中,只截取一小段并长期持有,那么这个大切片的底层数组将**永远不会被垃圾回收(GC)**。
```go
func getFirstTwo(largeSlice []byte) []byte {
// 错误做法:返回的切片与原大切片共享底层数组
return largeSlice[:2]
// 即使 largeSlice 本身被回收,只要返回的切片还在,整个大数组就在内存中
}
// ✅ 正确做法:使用 copy 创建一个独立的小切片
func getFirstTwoSafe(largeSlice []byte) []byte {
smallSlice := make([]byte, 2)
copy(smallSlice, largeSlice)
return smallSlice
}
```
---
### ❌ 陷阱 3:不能作为 map 的 key
与可比较的数组不同,切片是不可比较类型(因为它包含指针,且内容可变),因此不能作为 `map` 的键。
```go
package main
func main() {
// m := make(map[[]int]string) // 编译错误: invalid map key type []int
}
```
**替代方案**:可以将切片转换为字符串 `string(slice)` 作为 key,或者使用可比较的数组作为 key。
---
## 7. 最佳实践与性能建议
| 实践 | 说明 |
|------|------|
| ✅ 优先使用切片 | 除非你需要数组的特性(如 map key),否则总是使用切片。 |
| ✅ 预估容量 | 在创建切片时,如果能预估最终大小,使用 `make([]T, 0, capacity)` 来预分配容量,可极大减少 `append` 带来的扩容和复制开销。|
| ✅ `append` 后重新赋值 | 任何调用 `append` 的地方,都应将结果赋值回原切片:`s = append(s, …)`。 |
| ✅ `copy` 避免副作用 | 当需要一个完全独立的切片,或防止内存泄漏时,使用 `copy`。 |
| ✅ 警惕子切片共享数据 | 修改子切片可能会意外改变父切片或其他子切片的数据。 |
---
## 8. 典型应用场景
### 📌 场景 1:动态集合
任何需要动态增删元素的列表场景,如读取文件行、处理 HTTP 请求参数、解析 JSON 数组等。
```go
// 模拟从数据库读取不定数量的用户 ID
var userIDs []int
// for rows.Next() {
// var id int
// ...
// userIDs = append(userIDs, id)
// }
```
### 📌 场景 2:函数参数和返回值
作为函数参数传递可变数据集是切片最常见的用途,比传递数组指针更灵活、更安全。
```go
func ProcessItems(items []string) []string {
// ... 对 items 进行处理
return items
}
```
### 📌 场景 3:缓冲区(Buffer
在 I/O 操作中,切片被广泛用作读写缓冲区。
```go
// import "os"
// f, _ := os.Open("file.txt")
// buf := make([]byte, 1024)
// n, _ := f.Read(buf)
// data := buf[:n] // data 是实际读取到的有效数据
```
---
## 9. 扩容
### 切片扩容策略
```
1. 计算 newLen = oldLen + 新增数量
2. 调用 nextslicecap(newLen, oldCap) → 得到 newcap
├─ if newLen > oldCap*2 → 直接返回 newLen
├─ if oldCap < 256 → 返回 oldCap * 2
└─ else → 循环 newcap += (newcap + 768) / 4,直到 ≥ newLen
3. 计算所需内存:capmem = newcap * elemSize
4. 调用 roundupsize → 对齐到 mspan size class
5. 调整 newcap 以匹配对齐后的内存
6. mallocgc 分配新内存
7. memmove 复制旧数据 [0, oldLen)
8. 返回新切片 {新地址, newLen, newcap}
```
---
### 扩容机制细节
#### 1. 是否一定扩容?
不一定。扩容的条件是 `len == cap`。如果还有剩余容量,`append` 操作只是修改 `len`,不扩容。
```go
s := make([]int, 5, 10) // len=5, cap=10
s = append(s, 1, 2, 3) // len=8, cap=10,未扩容
```
#### 2. 扩容一定是新内存吗?
**是的。** 当触发扩容时,Go 运行时会:
- 分配一块**全新的、更大的底层数组**
- 将原数组数据**复制**到新数组
- 返回新的切片(指向新数组)
原有的切片引用失效,但如果有其他变量引用原底层数组,原数据不会立即被回收(除非无引用)。
```go
a := []int{1, 2, 3}
b := a // b 和 a 共享底层数组
a = append(a, 4) // a 扩容 → 底层数组复制,a 和 b 不再共享
```
#### 3. 内存对齐与类型影响
扩容计算还会考虑:
- 元素大小(如 `int``struct{}`
- 内存对齐规则
- 分配器策略(如 `runtime.growSlice`
因此实际扩容大小可能略大于 `1.25 * cap`,以便按对齐要求分配内存块。
---
## 总结
| 关键点 | 说明 |
|--------|------|
| 🧩 切片是数组的“视图” | 它是一个包含指针、长度和容量的结构 |
| 🔗 引用类型(行为上) | 赋值和传参共享底层数组,修改会相互影响 |
| 🚀 动态扩容 | `append` 是核心,当容量不足时会自动扩容(有性能开销)|
| ⚠️ 充满陷阱 | 需注意 `append` 的返回值、子切片内存泄漏和共享数据问题 |
| 🏆 Go主力数据结构 | 在日常开发中全面取代数组 |
> ✅ **推荐口诀**
> 切片是数组的窗,指针长度加容量。Append可能要搬家,函数返回新切片。共享内存要当心,拷贝数据保平安。
+427
View File
@@ -0,0 +1,427 @@
# 1. 指针基础:地址与解引用
指针(Pointer)是一个存储了另一个变量内存地址的变量。通过指针,我们可以间接地读取或修改其所指向变量的值。这是 Go 语言中实现引用传递、优化性能和构建复杂数据结构的基础。
## 1.1 什么是地址与指针?
在 Go 中,每个变量都存储在内存的特定位置,这个位置就是它的**内存地址**。**指针**就是专门用来存放这个地址的变量。
- **取地址 (`&`)**:使用 `&` 操作符可以获取一个变量的内存地址。
- **解引用 (`*`)**:使用 `*` 操作符可以获取指针所指向地址上存储的**值**。
```go
package main
import "fmt"
func main() {
// 声明一个整型变量 a
var a int = 42
// 1. 使用 & 操作符获取变量 a 的内存地址
// p 是一个指针变量,它的类型是 *int (读作“int的指针”)
// p 存储的是 a 的地址,所以我们说“p 指向 a”
var p *int = &a
// 2. 打印变量 a 的值和地址
// %p 用于格式化输出内存地址
fmt.Printf("变量 a 的值是: %d\n", a)
fmt.Printf("变量 a 的内存地址是: %p\n", &a)
// 3. 打印指针 p 自身的值(即 a 的地址)
fmt.Printf("指针 p 存储的地址是: %p\n", p)
// 4. 使用 * 操作符解引用,获取指针 p 指向地址上的值
// *p 的意思是“访问 p 指向的那个地址上的值”
fmt.Printf("通过指针 p 解引用得到的值是: %d\n", *p)
// 5. 通过指针修改变量 a 的值
fmt.Println("---通过指针修改值---")
*p = 100 // 将 p 指向的地址(即 a 的地址)上的值修改为 100
fmt.Printf("修改后,变量 a 的值变为: %d\n", a)
}
```
## 1.2 指针的声明与初始化
声明指针变量时,需要指定它将要指向的数据类型。Go 语言是类型安全的,一个 `*int` 类型的指针不能指向一个 `string` 类型的变量。
指针的零值为 `nil`。一个 `nil` 指针不指向任何内存地址。
```go
package main
import "fmt"
func main() {
// 准备一个变量 a 用于取地址
var a int = 10
// --- 三种常见的指针初始化方式 ---
// 方式一:先声明,后赋值(推荐 &a 方式)
// ptr1 是一个空指针,其值为 nil
var ptr1 *int
fmt.Printf("方式一(声明后):ptr1 的值是 %v\n", ptr1) // 输出: <nil>
ptr1 = &a
fmt.Printf("方式一(赋值后):ptr1 指向的值是 %d\n", *ptr1)
// 方式二:使用 new() 函数
// new(T) 会为类型 T 分配内存空间,并返回一个指向该空间的指针
// 该空间的值会被初始化为对应类型的零值
var ptr2 *int = new(int) // *ptr2 的初始值为 0
fmt.Printf("方式二(new):ptr2 指向的地址是 %p,值是 %d\n", ptr2, *ptr2)
*ptr2 = 20
fmt.Printf("方式二(赋值后):ptr2 指向的值是 %d\n", *ptr2)
// 方式三:在声明时直接使用 & 取地址(最常用)
var ptr3 *int = &a
fmt.Printf("方式三(取地址):ptr3 指向的值是 %d\n", *ptr3)
// 类型必须严格匹配
// var f float64 = 3.14
// ptr3 = &f // 这行代码会编译错误:cannot use &f (value of type *float64) as *int in assignment
}
```
# 2. 指针的常见应用场景
## 2.1 在函数间共享与修改数据
这是指针最核心的用途。Go 函数的参数默认是**值传递**(pass-by-value)。如果你想在函数内部修改外部变量的值,就需要传递这个变量的指针。
```go
package main
import "fmt"
// 该函数接收一个 int 指针
// 通过指针,它可以修改函数外部的原始变量
func modifyValue(ptr *int) {
fmt.Printf(" [函数内] 接收到的指针地址: %p\n", ptr)
*ptr = 100 // 修改指针指向地址上的值
}
func main() {
num := 10
fmt.Printf("调用函数前, num 的值: %d, 地址: %p\n", num, &num)
// 将 num 的地址传递给函数
modifyValue(&num)
fmt.Printf("调用函数后, num 的值: %d\n", num) // 值被成功修改
}
```
## 2.2 方法接收器 (Method Receiver)
在为结构体定义方法时,可以选择使用**值接收器**或**指针接收器**。
- **值接收器 (`func (p Person)`)**:方法操作的是结构体的副本,不会影响原始结构体。
- **指针接收器 (`func (p *Person)`)**:方法操作的是原始结构体的引用,可以修改原始结构体。
**[补充知识点]** **最佳实践**
1. 如果方法需要修改接收器的状态,必须使用指针接收器。
2. 如果接收器是大型结构体,为了避免每次方法调用都进行昂贵的值拷贝,推荐使用指针接收器。
3. 为了保持一致性,如果一个类型有一个指针接收器方法,那么其他方法也最好使用指针接收器。
```go
package main
import "fmt"
type Person struct {
name string
age int
}
// 值接收器: p 是 Person 的一个副本
func (p Person) updateAgeByValue(newAge int) {
p.age = newAge
fmt.Printf(" [值接收器内] p.age 被修改为 %d\n", p.age)
}
// 指针接收器: p 是指向原始 Person 实例的指针
func (p *Person) updateAgeByPointer(newAge int) {
p.age = newAge // Go 会自动解引用,等价于 (*p).age = newAge
fmt.Printf(" [指针接收器内] p.age 被修改为 %d\n", p.age)
}
func main() {
// 创建一个 Person 实例
user := Person{name: "Alice", age: 30}
fmt.Printf("原始 user: %+v\n", user)
fmt.Println("--- 调用值接收器方法 ---")
user.updateAgeByValue(31)
fmt.Printf("调用后,原始 user: %+v (未改变)\n\n", user)
fmt.Println("--- 调用指针接收器方法 ---")
// Go 语言很方便,可以直接用 user 调用指针方法,它会自动转换为 (&user)
user.updateAgeByPointer(35)
fmt.Printf("调用后,原始 user: %+v (已改变)\n", user)
}
```
## 2.3 构建复杂数据结构(如链表)
指针是构建链表、树、图等动态数据结构的核心。结构体可以包含指向同类型结构体的指针,从而将多个节点连接起来。
这个示例非常好,代码完整且清晰地展示了如何使用内嵌指针构建和操作一个简单的链表。我已按 `gofmt` 标准格式化。
```go
package main
import "fmt"
// Node 定义了链表中的一个节点(比喻为火车车厢)
type Node struct {
data int // 车厢里的货物编号
next *Node // 指向下一节车厢的指针
}
// printTrain 遍历并打印整个链表
func printTrain(head *Node) {
current := head
for current != nil {
fmt.Printf("车厢[%d] -> ", current.data)
current = current.next // 移动到下一个节点
}
fmt.Println("终点站")
}
func main() {
// 创建一个3节车厢的火车(链表)
// 1. 创建第一节车厢(火车头)
head := &Node{data: 1}
// 2. 连接第二节车厢
head.next = &Node{data: 2}
// 3. 连接第三节车厢
head.next.next = &Node{data: 3}
fmt.Println("初始火车:")
printTrain(head) // 输出: 车厢[1] -> 车厢[2] -> 车厢[3] -> 终点站
// 在火车头和第二节车厢之间插入一个新车厢
fmt.Println("\n在车厢[1]和[2]之间添加新车厢[4]:")
newNode := &Node{data: 4} // 准备新车厢
newNode.next = head.next // 新车厢连接到原来的第二节
head.next = newNode // 第一节连接到新车厢
fmt.Println("改造后的火车:")
printTrain(head) // 输出: 车厢[1] -> 车厢[4] -> 车厢[2] -> 车厢[3] -> 终点站
}
```
# 3. 指针的高级主题与辨析
## 3.1 多级指针
多级指针是指向指针的指针。例如,`**int` 是一个指向 `*int` 类型指针的指针。它在需要修改指针本身(而不是指针指向的值)的场景下非常有用。
```go
package main
import "fmt"
// 这个函数接收一个二级指针,用来修改一级指针的指向
func reassignPointer(ptr **int, newValue *int) {
*ptr = newValue
}
func main() {
// 原始变量
valueA := 10
valueB := 99
// 一级指针,指向 valueA
ptr1 := &valueA
fmt.Printf("初始时, ptr1 指向的地址是 %p, 值是 %d\n", ptr1, *ptr1)
// 二级指针,指向 ptr1
ptr2 := &ptr1
fmt.Printf("ptr2 指向的地址是 %p (即 ptr1 的地址)\n", ptr2)
fmt.Printf("通过 ptr2 两次解引用得到的值: %d\n\n", **ptr2)
// 目标:让 ptr1 不再指向 valueA,而是指向 valueB
// 我们需要修改 ptr1 本身的值,所以要传递 ptr1 的地址给函数
fmt.Println("--- 调用函数修改 ptr1 的指向 ---")
reassignPointer(&ptr1, &valueB) // 也可以写作 reassignPointer(ptr2, &valueB)
fmt.Printf("修改后, ptr1 指向的地址是 %p, 值是 %d\n", ptr1, *ptr1)
}
```
## 3.2 指针数组 vs 数组指针
这是一个常见的易混淆点,需要明确区分:
- **指针数组 `[N]*T`**:一个数组,其元素都是 `*T` 类型的指针。
- **数组指针 `*[N]T`**:一个指针,它指向一个大小为 `N` 的数组 `[N]T`
```go
package main
import "fmt"
func main() {
a, b, c := 10, 20, 30
// --- 1. 指针数组 ---
// 定义一个长度为3的数组,每个元素都是 *int 类型
// 它存储的是 a, b, c 各自的内存地址
pointerArray := [3]*int{&a, &b, &c}
fmt.Println("--- 指针数组 `[3]*int` ---")
for i, ptr := range pointerArray {
fmt.Printf(" 索引 %d: 指针地址 %p, 指向的值 %d\n", i, ptr, *ptr)
}
// 修改指针数组中某个指针指向的值
*pointerArray[0] = 100
fmt.Printf(" 修改后, 变量 a 的值变为: %d\n\n", a)
// --- 2. 数组指针 ---
// 定义一个普通的数组
arr := [3]int{1, 2, 3}
// 定义一个指针,它指向整个 arr 数组
var arrayPointer *[3]int = &arr
fmt.Println("--- 数组指针 `*[3]int` ---")
fmt.Printf(" 指针地址 %p, 指向的数组值 %v\n", arrayPointer, *arrayPointer)
// 通过数组指针修改数组元素
// Go 会自动解引用,arrayPointer[1] 等价于 (*arrayPointer)[1]
arrayPointer[1] = 200
fmt.Printf(" 修改后, 原始数组 arr 的值变为: %v\n", arr)
}
```
## 3.3 切片(Slice)与指针:深入辨析
这是一个非常重要的知识点,常常引起混淆。
切片本身就是一个包含指向底层数组的指针、长度(len)和容量(cap)的结构体。因此,当你将切片作为参数传递给函数时,实际上是复制了这个结构体。
- **`func(s []T)`(传切片)**:可以修改切片底层数组的元素,但如果在函数内使用 `append` 导致切片扩容,**外部的切片不会改变**。
- **`func(s *[]T)`(传切片指针)**:可以修改切片底层数组的元素,并且当 `append` 导致扩容时,可以**改变外部切片本身**(它的长度、容量和指向的底层数组)。
**结论**:只有当你需要在函数内部修改切片的长度或容量(比如使用 `append`)并希望这种改变反映到函数外部时,才需要使用指向切片的指针。
```go
package main
import "fmt"
// 接收切片本身。可以修改元素,但 append 的结果不会影响外部。
func modifySlice(s []int) {
s[0] = 99
// append 可能会创建一个新的底层数组,并返回一个新的切片头
s = append(s, 4)
fmt.Printf(" [函数内 modifySlice] 切片修改为: %v, len=%d, cap=%d\n", s, len(s), cap(s))
}
// 接收指向切片的指针。可以修改原始切片头。
func modifySliceByPointer(s *[]int) {
(*s)[0] = 999
// 对指针解引用后进行 append,会修改原始的切片头
*s = append(*s, 40)
fmt.Printf(" [函数内 modifySliceByPointer] 切片修改为: %v, len=%d, cap=%d\n", *s, len(*s), cap(*s))
}
func main() {
// --- 场景1: 直接传递切片 ---
slice1 := []int{1, 2, 3}
fmt.Printf("调用前 slice1: %v, len=%d, cap=%d\n", slice1, len(slice1), cap(slice1))
modifySlice(slice1)
// 元素修改成功,但 append 的效果丢失
fmt.Printf("调用后 slice1: %v, len=%d, cap=%d (append 无效)\n\n", slice1, len(slice1), cap(slice1))
// --- 场景2: 传递切片的指针 ---
slice2 := []int{10, 20, 30}
fmt.Printf("调用前 slice2: %v, len=%d, cap=%d\n", slice2, len(slice2), cap(slice2))
modifySliceByPointer(&slice2)
// 元素修改和 append 都成功
fmt.Printf("调用后 slice2: %v, len=%d, cap=%d (append 成功!)\n", slice2, len(slice2), cap(slice2))
}
```
# 4. 指针与内存管理
## 4.1 空指针(nil)的风险与防范
对一个 `nil` 指针进行解引用操作会导致程序 `panic`(运行时错误)。因此,在使用指针前进行 `nil` 检查是一个必须养成的良好习惯。
```go
package main
import "fmt"
// 一个安全的函数,它在解引用前检查指针是否为 nil
func safeGetValue(ptr *int) {
if ptr == nil {
fmt.Println("警告: 接收到一个空指针(nil),无法获取值。")
return
}
fmt.Printf("指针指向的值是: %d\n", *ptr)
}
func main() {
var p1 *int // p1 是一个空指针,值为 nil
var num int = 10
p2 := &num // p2 指向 num
fmt.Println("--- 尝试操作空指针 ---")
safeGetValue(p1)
fmt.Println("\n--- 尝试操作有效指针 ---")
safeGetValue(p2)
// 下面这行代码如果取消注释,将会导致 panic
// fmt.Println(*p1) // panic: runtime error: invalid memory address or nil pointer dereference
}
```
## 4.2 内存逃逸分析
Go 编译器会自动决定变量是分配在栈(stack)上还是堆(heap)上。
- **栈分配**:速度快,由编译器自动分配和释放。函数调用结束时,其栈上的变量就被销毁。
- **堆分配**:速度相对慢,需要垃圾回收器(GC)来管理和释放。
**逃逸分析(Escape Analysis** 是编译器用来决定变量分配位置的过程。如果一个局部变量的生命周期超出了其所在的函数(例如,被函数作为指针返回),它就会“逃逸”到堆上。
你可以使用 `go build -gcflags="-m"` 命令来观察逃逸分析的结果。
```go
package main
import "fmt"
// x 在函数返回后就不再被需要,因此分配在栈上
// 编译时会提示:stackAlloc() x does not escape
func stackAlloc() int {
x := 10
return x
}
// x 的内存地址被返回,在函数外部仍需访问
// 因此 x 必须“逃逸”到堆上,以保证在函数返回后依然有效
// 编译时会提示:heapAlloc() x escapes to heap
func heapAlloc() *int {
x := 10
return &x
}
func main() {
// a 的值是 10,来自栈上的拷贝
a := stackAlloc()
fmt.Printf("来自栈分配的值: %d\n", a)
// p 是一个指针,指向堆上的一块内存,其值为 10
p := heapAlloc()
fmt.Printf("来自堆分配的指针: %p, 指向的值: %d\n", p, *p)
// 这块由 heapAlloc 分配的内存,将由 Go 的垃圾回收器在不再被使用时自动清理
}
```
+463
View File
@@ -0,0 +1,463 @@
# 第 1 章:Go 语言的面向对象编程思想
它没有传统 OOP 语言(如 Java, C++)中的 `class``extends` 等关键字,但通过 `struct``method``interface` 的组合,为开发者提供了强大而灵活的面向对象编程能力。
在软件开发中,代码的组织遵循着一个自然的进化路径:
1. 函数(Function):最初,功能代码被聚合在独立的函数中。
2. 结构体(Struct):随着复杂度的增加,相关的变量和状态被组合成结构体,用于封装数据。
3. 方法(Method):将操作这些数据的函数(行为)与结构体(数据)绑定,就形成了方法,这已非常接近于传统 OOP 中“类”的概念。
# 第 2 章:核心构建块:类型、结构体与方法
## 2.1 `type` 关键字:定义新类型与类型别名
`type` 关键字是 Go 中构建自定义数据结构的基础。
1. **定义新类型**`type MyInt int`
- 创建了一个全新的、独立的类型 `MyInt`
- 它与 `int` 在类型系统上不兼容,需要显式转换才能混合运算。
2. **定义类型别名**`type AliasInt = int`
- `AliasInt` 只是 `int` 的一个别名,它们是完全相同的类型。
- 主要用于提高代码可读性,在 Go 1.9 版本引入。`any` 就是 `interface{}` 的类型别名。
```go
package main
import "fmt"
// 1. 定义一个全新的类型 MyInt
type MyInt int
// 2. 为 int 类型定义一个别名 Number
// 在 Go 1.18 之后,any 就是 interface{} 的类型别名
type Number = int
func main() {
// --- 新类型示例 ---
var a MyInt = 10
var b int = 20
// a 和 b 是不同类型,直接相加会编译错误
// fmt.Println(a + b) // a + b (mismatched types MyInt and int)
// 必须将 MyInt 类型的 a 转换为 int 类型
fmt.Printf("新类型 MyInt 转换为 int 后与 int 相加: %d\n", int(a)+b)
fmt.Printf("类型 a: %T, 类型 b: %T\n", a, b)
fmt.Println("--------------------")
// --- 类型别名示例 ---
var x Number = 30
var y int = 40
// x 和 y 本质上都是 int 类型,可以直接运算
fmt.Printf("类型别名 Number 与 int 直接相加: %d\n", x+y)
fmt.Printf("类型 x: %T, 类型 y: %T\n", x, y)
}
```
## 2.2 `struct`:数据聚合的基石
`struct` (结构体)是 Go 中聚合零个或多个任意类型字段的数据集合,它是构建复杂数据结构和实现面向对象编程中“数据”部分的基础。
```go
package main
import "fmt"
// 定义一个 Person 结构体,用于封装人的属性
type Person struct {
Name string
Age int
}
func main() {
// 创建并初始化一个 Person 实例
p1 := Person{
Name: "Alice",
Age: 30,
}
fmt.Printf("创建的 Person 实例: %+v\n", p1)
// 访问结构体的字段
fmt.Printf("姓名: %s, 年龄: %d\n", p1.Name, p1.Age)
}
```
## 2.3 `method`:为类型绑定行为
方法(Method)是绑定到特定类型的函数。它将数据(struct)和操作数据的行为(function)结合起来。
- **函数 (Function)**: `func DoSomething()`,独立调用。
- **方法 (Method)**: `func (t MyType) DoSomething()`,通过类型的实例 `instance.DoSomething()` 调用。
方法的接收者可以是**值类型**或**指针类型**。
```go
package main
import "fmt"
// 定义一个矩形结构体
type Rectangle struct {
width, height float64
}
// 1. 值接收者方法 (Value Receiver)
// 接收者 r 是 Rectangle 的一个副本。
// 方法内部对 r 的修改不会影响原始的 Rectangle 实例。
// 适用于不需要修改原始数据,并且结构体较小的场景。
func (r Rectangle) Area() float64 {
// r.width = 100 // 这里的修改是无效的,因为 r 只是一个副本
return r.width * r.height
}
// 2. 指针接收者方法 (Pointer Receiver)
// 接收者 r 是一个指向 Rectangle 实例的指针。
// 方法内部对 *r 的修改会直接影响原始的 Rectangle 实例。
// 适用于需要修改原始数据,或结构体较大以避免复制开销的场景。
func (r *Rectangle) Scale(factor float64) {
r.width *= factor
r.height *= factor
}
func main() {
rect := Rectangle{width: 10, height: 5}
// 调用值接收者方法
fmt.Printf("原始矩形: %+v\n", rect)
fmt.Printf("面积: %.2f\n", rect.Area())
// 调用指针接收者方法
// Go 语言会自动进行转换,rect.Scale(2) 等效于 (&rect).Scale(2)
rect.Scale(2)
fmt.Printf("缩放后的矩形: %+v\n", rect)
fmt.Printf("缩放后的面积: %.2f\n", rect.Area())
}
```
## 值接收者 vs. 指针接收者选择指南
1. **要修改接收者的状态吗?**
- **是** -> **必须**使用指针接收者 (`*T`)。
- **否** -> 可以使用值接收者 (`T`)。
2. **性能考虑**
- 如果接收者是一个**大型结构体**或数组,使用**指针接收者**可以避免昂贵的内存拷贝。
3. **一致性**
- 如果一个类型已经有了指针接收者的方法,那么其他方法也建议使用指针接收者以保持一致。
4. **特殊类型**
- 如果类型包含 `sync.Mutex` 或其他不应被复制的同步字段,**必须**使用指针接收者。
# 第 3 章:代码复用:组合与内嵌
Go 语言通过结构体**内嵌**Embedding)和**组合**Composition)实现代码复用,而不是传统的类继承。
## 3.1 结构体内嵌(模拟继承 - is-a 关系)
通过在结构体中**匿名地**嵌入另一个结构体类型,可以获得被嵌入结构体的所有字段和方法,这在形式上模拟了“继承”。
```go
package main
import "fmt"
// "父类":动物
type Animal struct {
name string
}
// Animal 的方法
func (a *Animal) Move() {
fmt.Printf("%s is moving.\n", a.name)
}
// "子类":狗
type Dog struct {
Animal // 匿名内嵌 AnimalDog is an Animal
breed string
}
// Dog 重写了 Move 方法
func (d *Dog) Move() {
fmt.Printf("%s the %s is running happily!\n", d.name, d.breed)
}
// Dog 特有的方法
func (d *Dog) Bark() {
fmt.Printf("%s says: Woof! Woof!\n", d.name)
}
func main() {
// 创建一个 Dog 实例
dog := &Dog{
Animal: Animal{name: "Buddy"},
breed: "Golden Retriever",
}
// 1. 可以直接访问内嵌结构体的字段
fmt.Printf("Dog's name is: %s\n", dog.name)
// 2. 调用 Dog 自己重写的方法 (就近原则)
dog.Move()
// 3. 调用内嵌结构体的原始方法 (需要显式指定)
dog.Animal.Move()
// 4. 调用 Dog 特有的方法
dog.Bark()
}
```
## 3.2 组合(has-a 关系)
组合是通过在结构体中包含一个**具名**的字段来实现的。这表示一个类型“拥有”另一个类型的实例。 **Go 语言哲学:优先使用组合而不是继承。**
```go
package main
import "fmt"
// 定义引擎结构体
type Engine struct {
horsepower int
}
// 引擎的方法
func (e *Engine) Start() {
fmt.Printf("Engine with %d HP starts. Vroom!\n", e.horsepower)
}
// 定义汽车结构体
type Car struct {
brand string
engine Engine // 具名字段,Car has an Engine
}
func main() {
// 创建一个 Car 实例,它“拥有”一个 Engine
myCar := Car{
brand: "Ferrari",
engine: Engine{
horsepower: 710,
},
}
fmt.Printf("My car is a %s.\n", myCar.brand)
// 访问组合部件的方法需要通过字段名
myCar.engine.Start()
}
```
# 第 4 章:多态的基石:接口
接口(Interface)是 Go 实现多态的核心。它定义了一组方法的集合(契约),任何类型只要实现了接口中所有的方法,就被认为是该接口的实现,这个过程是**隐式**的。
## 4.1 接口的定义与实现(多态)
多态即“多种形态”。一个变量可以持有实现了同一接口的不同类型的值,并在运行时调用相应类型的方法。
```go
package main
import "fmt"
// 1. 定义一个接口 (契约)
// Shaper 接口定义了一个行为:计算面积
type Shaper interface {
Area() float64
}
// 2. 定义实现该接口的多个结构体
type Circle struct {
radius float64
}
// Circle 实现了 Shaper 接口
func (c Circle) Area() float64 {
return 3.14159 * c.radius * c.radius
}
type Square struct {
side float64
}
// Square 实现了 Shaper 接口
func (s Square) Area() float64 {
return s.side * s.side
}
// 3. 定义一个接收接口类型参数的函数,实现多态
func PrintArea(s Shaper) {
fmt.Printf("This shape is of type %T and its area is %.2f\n", s, s.Area())
}
func main() {
// 创建不同类型的实例
circle := Circle{radius: 5}
square := Square{side: 4}
// 使用多态函数
// 尽管 circle 和 square 是不同类型
// 但它们都满足 Shaper 接口,所以可以被 PrintArea 函数接收
PrintArea(circle)
PrintArea(square)
// 接口变量可以持有任何实现了该接口的类型的值
var shaper Shaper
shaper = circle
fmt.Printf("Shaper variable holding a Circle: Area=%.2f\n", shaper.Area())
shaper = square
fmt.Printf("Shaper variable holding a Square: Area=%.2f\n", shaper.Area())
}
```
## 4.2 空接口 (`interface{}` 或 `any`)
空接口不包含任何方法,因此**所有类型都默认实现了空接口**。它可以用来存储任意类型的值。自 Go 1.18 起,`any` 成为了 `interface{}` 的官方别名。
```go
package main
import "fmt"
// 这个函数可以接收并打印任何类型的值
func printAnything(v any) { // 使用 any 代替 interface{}
fmt.Printf("Value: %v, Type: %T\n", v, v)
}
func main() {
printAnything(42)
printAnything("Hello, Go!")
printAnything(true)
printAnything(3.14)
// 在集合类型中非常有用
heterogeneousSlice := []any{"text", 100, false, 2.718}
fmt.Println("\nA slice with mixed types:", heterogeneousSlice)
hetegeneousMap := map[string]any{
"name": "Excalicode",
"version": 1.0,
"active": true,
}
fmt.Println("A map with mixed value types:", hetegeneousMap)
}
```
## 4.3 接口断言(Type Assertion
接口断言用于在运行时检查一个接口类型变量所持有的具体类型,并获取其底层值。
1. **安全形式**`value, ok := i.(Type)`。如果断言成功,`ok``true``value` 为具体类型的值;如果失败,`ok``false``value` 为该类型的零值。**这是推荐的方式**。
2. **非安全形式**`value := i.(Type)`。如果断言失败,程序会发生 `panic`
3. **Type Switch**`switch v := i.(type)`。用于判断多种可能的类型。
```go
package main
import "fmt"
func process(v any) {
fmt.Printf("\nProcessing value: %v\n", v)
// 1. 安全的类型断言
str, ok := v.(string)
if ok {
fmt.Printf("It's a string! Value: \"%s\"\n", str)
return
}
// 2. 使用 Type Switch 处理多种类型
switch t := v.(type) {
case int:
fmt.Printf("It's an int! Value: %d\n", t)
case bool:
fmt.Printf("It's a bool! Value: %v\n", t)
case float64:
fmt.Printf("It's a float64! Value: %.2f\n", t)
default:
fmt.Printf("It's an unknown type: %T\n", t)
}
}
func main() {
process("Go is awesome")
process(2024)
process(true)
process(3.14159)
process([]int{1, 2, 3})
}
```
## 4.4 接口嵌套(Interface Embedding
一个接口可以嵌入其他接口,从而组合它们的方法集。一个类型必须实现所有被嵌入接口的方法,才能满足这个组合后的大接口。
```go
package main
import "fmt"
// 定义基础接口
type Reader interface {
Read() string
}
type Writer interface {
Write(data string)
}
// 接口嵌套:ReadWriter 组合了 Reader 和 Writer
type ReadWriter interface {
Reader
Writer
}
// 定义一个实现 ReadWriter 接口的结构体
type MemoryBuffer struct {
data string
}
// 实现 Reader 接口
func (mb *MemoryBuffer) Read() string {
return mb.data
}
// 实现 Writer 接口
func (mb *MemoryBuffer) Write(data string) {
mb.data = data
fmt.Printf("Wrote '%s' to buffer.\n", data)
}
func main() {
// 创建实例
buffer := &MemoryBuffer{}
// buffer 既是 Reader, 也是 Writer, 也是 ReadWriter
// 1. 使用 ReadWriter 接口
var rw ReadWriter = buffer
rw.Write("Hello, nested interfaces!")
fmt.Printf("Read from ReadWriter: %s\n", rw.Read())
// 2. 也可以作为 Reader 接口使用
var r Reader = buffer
fmt.Printf("Read from Reader: %s\n", r.Read())
// 3. 也可以作为 Writer 接口使用
var w Writer = buffer
w.Write("New data")
fmt.Printf("Read again after writing via Writer interface: %s\n", rw.Read())
}
```
## 接口使用的最佳实践
1. **小接口,大作为**:倾向于定义小而专一的接口(例如 `io.Reader`),而不是一个包含几十个方法的大而全的接口。Go 的名言是:“The bigger the interface, the weaker the abstraction.”(接口越大,抽象能力越弱)。
2. **`er` 命名约定**:对于只包含一个方法的接口,通常在方法名后加上 `er` 后缀来命名,如 `Reader`, `Writer`, `Stringer`
3. **接收接口,返回结构体 (Accept Interfaces, Return Structs)**:这是一个重要的 Go 设计模式。函数参数应尽量使用接口类型,增加灵活性和可测试性;而返回值应尽量是具体的结构体类型,让调用者清楚地知道他们得到了什么。
@@ -0,0 +1,440 @@
# Go 语言数组详解:从基础到实践
在 Go 语言中,**数组(Array)** 是最基本、最底层的数据结构之一,虽然在实际开发中我们更多使用切片(Slice),但理解数组对于深入掌握 Go 的内存模型、值传递机制和性能优化至关重要。
---
## 1. 数组基础概念
数组是 Go 中一种**固定长度、类型一致、有序存储**的集合类型。
### 核心特征
-**类型一致性**:所有元素必须是相同类型
-**固定长度**:声明后长度不可变(编译期确定)
-**值类型**:赋值和传参时会进行**深拷贝**
-**连续内存布局**:元素在内存中连续存放,有利于缓存命中
> ⚠️ 注意:`[3]int` 和 `[5]int` 是两种完全不同的类型!
---
## 2. 数组声明与初始化
### 示例 1:基本声明与初始化
```go
package main
import "fmt"
func main() {
var arr1 [5]int // 声明但未初始化,元素默认为零值
var arr2 = [5]int{1, 2, 3, 4, 5} // 显式初始化
arr3 := [5]int{10, 20, 30, 40, 50} // 简短声明
fmt.Println("arr1:", arr1) // [0 0 0 0 0]
fmt.Println("arr2:", arr2) // [1 2 3 4 5]
fmt.Println("arr3:", arr3) // [10 20 30 40 50]
}
```
---
### 示例 2:自动推导长度 `…`
Go 支持使用 `…` 让编译器自动推导数组长度。
```go
package main
import "fmt"
func main() {
arr := [...]int{1, 2, 3, 4, 5} // 自动推断为 [5]int
fmt.Println("数组长度:", len(arr)) // 输出:5
fmt.Println("数组内容:", arr) // [1 2 3 4 5]
// 注意:一旦推导完成,长度依然是固定的
}
```
---
### 示例 3:指定索引初始化(稀疏数组风格)
可以跳过某些位置初始化,未指定的位置将使用零值填充。
```go
package main
import "fmt"
func main() {
arr := [10]int{1: 100, 5: 500} // 第1和第5个元素被赋值
fmt.Println("指定索引初始化:", arr) // [0 100 0 0 0 500 0 0 0 0]
}
```
---
### 示例 4:复合类型的数组(结构体为例)
数组不仅能存基本类型,还能存储结构体、指针、数组等复合类型。
```go
package main
import "fmt"
type Student struct {
Name string
Age int
}
func main() {
students := [2]Student{
{"Alice", 20},
{"Bob", 22},
}
fmt.Println("学生数组:", students) // [{Alice 20} {Bob 22}]
}
```
---
## 3. 数组操作详解
### 示例 5:获取长度与容量
数组的 `len()``cap()` 返回值总是相等,因为容量 == 长度。
```go
package main
import "fmt"
func main() {
arr := [5]string{"a", "b", "c"}
fmt.Printf("长度: %d\n", len(arr)) // 5
fmt.Printf("容量: %d\n", cap(arr)) // 5
}
```
---
### 示例 6:安全访问与修改元素
越界访问会导致 panic
```go
package main
import "fmt"
func main() {
arr := [3]int{10, 20, 30}
// 访问
fmt.Println("第一个元素:", arr[0]) // 10
// 修改
arr[1] = 99
fmt.Println("修改后:", arr) // [10 99 30]
// ❌ 错误示例(会 panic)—— 超出范围
// arr[5] = 100 // panic: runtime error: index out of range
}
```
---
### 示例 7:遍历数组的三种方式
```go
package main
import "fmt"
func main() {
arr := [4]int{1, 3, 5, 7}
fmt.Println("1. 使用索引遍历:")
for i := 0; i < len(arr); i++ {
fmt.Printf("arr[%d] = %d\n", i, arr[i])
}
fmt.Println("\n2. 使用 range 索引:")
for i := range arr {
fmt.Printf("arr[%d] = %d\n", i, arr[i])
}
fmt.Println("\n3. 使用 range 获取索引和值:")
for idx, val := range arr {
fmt.Printf("索引 %d => 值 %d\n", idx, val)
}
fmt.Println("\n4. 忽略索引,只获取值:")
for _, val := range arr {
fmt.Printf("值: %d ", val)
}
fmt.Println()
}
```
---
## 4. 多维数组详解
### 示例 8:二维数组的声明与遍历
```go
package main
import "fmt"
func main() {
// 声明一个 3x4 的二维数组
matrix := [3][4]int{
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11},
}
fmt.Println("二维数组内容:")
for i := 0; i < len(matrix); i++ {
for j := 0; j < len(matrix[i]); j++ {
fmt.Printf("%4d", matrix[i][j])
}
fmt.Println()
}
}
```
> 输出:
```
0 1 2 3
4 5 6 7
8 9 10 11
```
---
### 示例 9:三维及以上数组(了解)
虽然不常用,但 Go 支持高维数组。
```go
package main
import "fmt"
func main() {
cube := [2][3][2]int{
{{1, 2}, {3, 4}, {5, 6}},
{{7, 8}, {9, 10}, {11, 12}},
}
fmt.Println("三维数组 [0][1][1]:", cube[0][1][1]) // 输出:4
}
```
---
## 5. 数组作为值类型的特性
这是 Go 数组最重要的特性之一:**值类型 vs 引用类型**
### 示例 10:数组赋值产生副本
```go
package main
import "fmt"
func main() {
arr1 := [4]int{1, 2, 3, 4}
arr2 := arr1 // 完整复制一份(深拷贝)
arr2[0] = 999
fmt.Println("arr1:", arr1) // [1 2 3 4] — 不受影响
fmt.Println("arr2:", arr2) // [999 2 3 4]
}
```
---
### 示例 11:函数传参时的复制行为
```go
package main
import "fmt"
func modify(arr [3]int) {
arr[0] = 999
fmt.Println("函数内 arr:", arr) // [999 2 3]
}
func main() {
original := [3]int{1, 2, 3}
modify(original)
fmt.Println("函数外 original:", original) // [1 2 3] — 未改变!
}
```
> ✅ 解决方案:若需修改原始数据,应传指针:
```go
func modifyPtr(arr *[3]int) {
arr[0] = 999 // 等价于 (*arr)[0]
}
// 调用:modifyPtr(&original)
```
---
## 6. 数组的常见陷阱与注意事项
### ❌ 陷阱 1:不同长度的数组不能赋值
```go
package main
func main() {
// var a [3]int
// var b [4]int
// a = b // 编译错误:cannot use b (type [4]int) as type [3]int
}
```
虽然都是整型数组,但 `[3]int``[4]int`,属于不同类型。
---
### ❌ 陷阱 2:大型数组传递性能差
如果数组很大(如 `[10000]int`),每次传参会复制整个数组,造成严重性能问题。
✅ 正确做法:使用指针。
```go
package main
import "fmt"
func process(data *[1000]int) {
// 直接操作原数据,无复制开销
data[0] = 100
}
func main() {
largeArr := [1000]int{}
process(&largeArr)
fmt.Println("largeArr[0]:", largeArr[0]) // 100
}
```
---
### ❌ 陷阱 3:无法动态扩容
数组长度固定,不能追加元素。
✅ 替代方案:使用切片(slice),它是对数组的抽象封装。
```go
slice := []int{1, 2, 3}
slice = append(slice, 4) // ✅ 可以动态增长
```
---
## 7. 最佳实践与性能建议
| 实践 | 说明 |
|------|------|
| ✅ 优先使用切片 | 绝大多数场景下应使用 `[]int` 而非 `[5]int` |
| ✅ 小数组可用值传递 | 若长度小(如 `[3]float64` 表示坐标),值传递更安全高效 |
| ✅ 大数组用指针传递 | 避免不必要的复制开销 |
| ✅ 利用连续内存优势 | 数组适合科学计算、图像处理等需缓存友好的场景 |
| ✅ 多维数组注意初始化格式 | 特别是混合维度时容易出错 |
---
## 8. 典型应用场景
### 📌 场景 1:固定配置参数
如 RGB 颜色表示:
```go
var color [3]byte = [3]byte{255, 0, 128}
```
### 📌 场景 2:哈希表键(map key)
数组如果是可比较类型,可作为 map 的 key,而切片不能。
```go
package main
import "fmt"
func main() {
// 使用 [2]int 作为 map 键
coordMap := make(map[[2]int]string)
coordMap[[2]int{0, 0}] = "origin"
coordMap[[2]int{3, 4}] = "point A"
fmt.Println(coordMap) // map[[0 0]:origin [3 4]:point A]
}
```
> ✅ 注意:只有**可比较类型**的数组才能做 key(如 `[2]int`),包含 `slice`、`map`、`func` 的数组不行。
---
### 📌 场景 3:性能敏感的底层计算
如矩阵运算、密码学哈希中的定长缓冲区:
```go
package main
import (
"crypto/sha256"
"fmt"
)
func main() {
data := []byte("hello")
hash := sha256.Sum256(data) // 返回 [32]byte 类型
fmt.Printf("SHA256: %x\n", hash)
fmt.Printf("类型是: %T\n", hash) // [32]uint8
}
```
---
## 总结
| 关键点 | 说明 |
|--------|------|
| 🧩 数组是 Go 的基础结构 | 了解它是理解切片的前提 |
| 🔐 值类型带来安全性 | 避免意外修改,但也带来开销 |
| ⚡ 连续内存提升性能 | 适合高性能计算场景 |
| 🚫 无法扩容 | 日常开发建议用 `slice` 代替 |
| 💡 适用于小而固定的集合 | 如坐标、颜色、哈希值等 |
> ✅ **推荐口诀**
> 小而固定用数组,大而可变用切片;传参小心复制坑,指针优化性能佳。
+243
View File
@@ -0,0 +1,243 @@
# 1. Map 的本质与内部实现
Map 是 Go 语言中一种核心的数据结构,它用于存储键值对(key-value)的无序集合。
- **本质**: 它在其他语言中通常被称为哈希表(Hash Table)或字典(Dictionary)。
- **内部实现**: Go 的 map 底层是通过哈希表实现的。通过一个哈希函数,将键(key)计算成一个哈希值,然后根据这个哈希值定位到存储桶(bucket)来存放对应的值(value)。这使得 map 在平均情况下的**增、删、查操作的时间复杂度都能达到 O(1)**。
# 2. Map 的创建与初始化
创建 map 是使用它的第一步,不同的创建方式适用于不同的场景。
## 方式 1:使用 `make` 函数(推荐)
这是最常用的方式。可以指定类型,并可选地指定初始容量。
```go
package main
import "fmt"
func main() {
// 使用 make 创建一个空的 map
// 此时 m2 不是 nil,是一个长度为 0 的 map,可以直接使用
m2 := make(map[string]int)
m2["math"] = 90
fmt.Printf("m2: %#v, len: %d\n", m2, len(m2)) // 输出: m2: map[string]int{"math":90}, len: 1
// [补充知识点] 预估容量可以提高性能,避免后续操作中的内存重新分配
// 创建一个容量为 10 的 map,但当前长度仍为 0
m3 := make(map[string]int, 10)
fmt.Printf("m3: %#v, len: %d\n", m3, len(m3)) // 输出: m3: map[string]int{}, len: 0
}
```
## 方式 2:使用字面量 literal 初始化
如果在创建时就能确定一些初始键值对,这种方式非常直观。
```go
package main
import "fmt"
func main() {
// 在声明时直接提供初始键值对
scores := map[string]int{
"张三": 95,
"李四": 88,
"王五": 79,
}
fmt.Printf("scores: %#v, len: %d\n", scores, len(scores))
}
```
## 方式 3:仅声明(**注意陷阱**)
这种方式会创建一个 `nil map`,它不能直接用于存储键值对,否则会引发 `panic`
```go
package main
import "fmt"
func main() {
// 仅声明一个 map 变量,其零值为 nil
var m1 map[string]int
fmt.Printf("m1 is nil: %v\n", m1 == nil) // 输出: m1 is nil: true
// 对 nil map 进行读操作是安全的,会返回零值
score := m1["张三"]
fmt.Printf("从 nil map 读取 '张三' 的分数: %d\n", score) // 输出: 0
// !!! 错误操作:对 nil map 进行写操作会引发 panic
// m1["张三"] = 100 // a panic: assignment to entry in nil map
// 正确做法:在使用前必须先用 make 进行初始化
m1 = make(map[string]int)
m1["张三"] = 100
fmt.Printf("初始化后: %#v\n", m1)
}
```
# 3. Map 的核心操作
Map 的基本操作包括增加/更新、查询、删除和判断键是否存在。
```go
package main
import "fmt"
func main() {
scores := make(map[string]int)
// 1. 增加/更新元素
// 如果键 "小明" 不存在,则为增加;如果已存在,则为更新
scores["小明"] = 90
fmt.Println("增加后:", scores)
scores["小明"] = 100 // 更新
fmt.Println("更新后:", scores)
// 2. 获取元素
// 直接获取,如果键不存在,会得到该值类型的零值(对于 int 是 0)
score := scores["小明"]
fmt.Println("小明的分数:", score)
scoreUnknown := scores["小红"] // "小红" 不存在
fmt.Println("小红的分数 (零值):", scoreUnknown)
// 3. 判断键是否存在("comma ok" idiom
// 这是判断键是否存在的标准方式,推荐使用
score, exists := scores["小明"]
if exists {
fmt.Printf("小明的分数存在: %d\n", score)
} else {
fmt.Println("小明的分数不存在")
}
_, exists = scores["小红"] // 可以忽略第一个返回值(值本身)
if !exists {
fmt.Println("小红的分数不存在")
}
// 4. 删除元素
// 使用 delete() 内置函数,如果键不存在,此操作不会产生任何效果,也不会报错
delete(scores, "小明")
fmt.Println("删除后:", scores)
delete(scores, "不存在的键") // 安全操作
}
```
# 4. 遍历 Map
使用 `for…range` 循环可以遍历 map 的所有键值对。
**重要**:Map 的遍历顺序是**随机**的,Go 语言在设计上特意如此,以防止开发者依赖于某个固定的遍历顺序。
```go
package main
import "fmt"
func main() {
scores := map[string]int{
"张三": 95,
"李四": 88,
"王五": 79,
"赵六": 92,
}
fmt.Println("--- 遍历键值对 ---")
// 每次运行的输出顺序可能不同
for name, score := range scores {
fmt.Printf("姓名: %s, 分数: %d\n", name, score)
}
fmt.Println("\n--- 只遍历键 ---")
for name := range scores {
fmt.Printf("姓名: %s\n", name)
}
// [补充知识点] 如果需要有序遍历,需要先提取所有的键,对键进行排序,然后根据排序后的键进行遍历。
// 这部分内容将在排序相关章节详细讲解。
}
```
# 5. 核心特性与常见陷阱 (重点)
## 5.1 Map 是引用类型
将 map 变量赋值给另一个变量,或作为函数参数传递时,它们都指向**同一个**底层数据结构。修改其中一个会影响到另一个。
```go
package main
import "fmt"
// modifyMap 函数会修改传入的 map
func modifyMap(m map[string]int) {
m["extra"] = 1000
}
func main() {
originalMap := map[string]int{"a": 1, "b": 2}
fmt.Println("原始 map:", originalMap)
// 将 map 作为参数传递
modifyMap(originalMap)
// originalMap 的内容被修改了
fmt.Println("函数调用后的 map:", originalMap)
}
```
## 5.2 键的类型限制
Map 的键(key)必须是**可比较的**类型,否则会在编译时报错。
- **可比较的类型**:布尔型、数字类型、字符串、指针、通道、接口,以及只包含可比较字段的结构体和数组。
- **不可比较的类型**:切片(slice)、map、函数。这些类型不能作为 map 的键。
## 5.3 并发不安全
Go 内置的 map 类型**不是并发安全的**。如果在多个 goroutine 中同时对一个 map 进行读写操作,而不加任何同步控制,会导致程序崩溃(panic)。
- **解决方案**: 若需要在并发环境中使用 map,应该使用 `sync.Mutex``sync.RWMutex` 进行加锁保护,或者使用 Go 1.9 之后提供的 `sync.Map` 类型。`sync.Map` 专门为“读多写少”的并发场景进行了优化。
# 6. 实战示例:统计字符串中各字符出现的次数
这是一个非常经典的 map 应用场景,您的笔记中提供的示例已经很好了,我这里将其格式化为一个更完整的可运行程序,并添加了更详细的注释。
```go
package main
import "fmt"
// countChars 函数接收一个字符串,返回一个 map,记录每个字符出现的次数。
// 使用 rune 类型作为键,可以正确处理多字节的 Unicode 字符(如汉字)。
func countChars(s string) map[rune]int {
// 创建一个 map 用于存储结果
charCount := make(map[rune]int)
// 遍历字符串中的每一个字符 (rune)
for _, char := range s {
// map 的一个优雅之处:如果键不存在,map[key] 的访问会返回零值(int 的零值是 0)
// 所以可以直接进行 ++ 操作,无需预先检查键是否存在。
charCount[char]++
}
return charCount
}
func main() {
text := "Go语言, a great language!"
result := countChars(text)
fmt.Printf("字符串 \"%s\" 的字符统计结果如下:\n", text)
for char, count := range result {
// 使用 %c 格式化字符,%d 格式化次数
fmt.Printf("字符 '%c' 出现了 %d 次\n", char, count)
}
}
```
@@ -0,0 +1,489 @@
# 1. 结构体定义与初始化
结构体通过 `type``struct` 关键字定义。初始化实例有多种灵活的方式。
## 1.1 定义语法
```go
type StructName struct {
FieldName1 FieldType1
FieldName2 FieldType2
// ...
}
```
## 1.2 初始化实例
以下是四种最常见的初始化结构体的方式,每种方式都有其适用的场景。
```go
// [代码规范化]:这是一个完整的、可运行的示例,整合了所有初始化方式。
package main
import "fmt"
// Person 定义了一个代表“人”的结构体
type Person struct {
name string
age int
}
func main() {
fmt.Println("--- 结构体初始化示例 ---")
// 方式1:先声明,后赋值 (零值初始化)
// 声明一个 Person 类型的变量 p1,此时所有字段都是其类型的零值。
// string 的零值是 ""int 的零值是 0。
var p1 Person
p1.name = "张三"
p1.age = 25
fmt.Printf("方式1 - 声明后赋值: %+v\n", p1) // %+v 可以打印字段名和值
// 方式2:使用字面量并指定字段名 (推荐)
// 这是最常用、最清晰、最不易出错的方式,因为字段顺序的改变不影响初始化。
p2 := Person{
name: "李四",
age: 30,
}
fmt.Printf("方式2 - 字段名初始化: %+v\n", p2)
// 方式3:使用字面量,按顺序提供字段值
// [补充知识点] 常见陷阱:这种方式代码脆弱,如果结构体定义中字段顺序改变,
// 此处的初始化代码必须同步修改,否则会导致编译错误或逻辑错误。
p3 := Person{"王五", 35}
fmt.Printf("方式3 - 顺序初始化: %+v\n", p3)
// 方式4:使用 new 关键字创建结构体指针 (详见下一节)
// new(Person) 会分配内存,并将所有字段初始化为零值,然后返回一个指向该内存的指针。
p4 := new(Person)
p4.name = "赵六"
p4.age = 40
fmt.Printf("方式4 - 使用 new 创建指针: %+v\n", *p4) // 注意需要解引用
}
```
# 2. 结构体指针 (Struct Pointers)
在 Go 中,直接传递结构体会产生一次完整的内存拷贝。对于大型结构体,这会影响性能。使用结构体指针可以避免拷贝,并且允许函数直接修改原始结构体实例。
```go
// [代码规范化]:这是一个完整的、可运行的示例,清晰地对比了值传递和指针传递。
package main
import "fmt"
// Student 定义了一个学生结构体
type Student struct {
name string
age int
}
// updateAgeByValue 接收一个结构体值(副本)
// 在函数内对 s 的任何修改都不会影响原始的 Student 变量。
func updateAgeByValue(s Student, newAge int) {
s.age = newAge
fmt.Printf("-> 在值传递函数内,年龄被修改为: %d\n", s.age)
}
// updateAgeByPointer 接收一个结构体指针
// 在函数内对 s 的修改会直接作用于原始的 Student 变量。
func updateAgeByPointer(s *Student, newAge int) {
// Go 语言提供了语法糖,可以直接用 s.age 访问,
// 无需写成 (*s).age。编译器会自动处理。
s.age = newAge
fmt.Printf("-> 在指针传递函数内,年龄被修改为: %d\n", s.age)
}
func main() {
fmt.Println("--- 结构体指针与值传递对比 ---")
// 场景1: 值传递
s1 := Student{name: "小明", age: 18}
fmt.Printf("原始 s1: %+v\n", s1)
updateAgeByValue(s1, 20)
fmt.Printf("调用值传递函数后,原始 s1: %+v (未改变)\n\n", s1)
// 场景2: 指针传递
s2 := &Student{name: "小红", age: 22} // 使用 & 获取结构体实例的指针
fmt.Printf("原始 s2: %+v\n", *s2)
updateAgeByPointer(s2, 25)
fmt.Printf("调用指针传递函数后,原始 s2: %+v (已改变)\n", *s2)
}
```
# 3. 结构体方法 (Methods)
方法是附加到特定类型(接收者)上的函数。Go 使用方法为结构体添加行为。
## 3.1 值接收者 vs. 指针接收者
方法的接收者可以是值类型,也可以是指针类型,选择哪种类型是 Go 设计中的一个重要考量点。
| 特性 | 值接收者 (`func (r MyType) Method()`) | 指针接收者 (`func (r *MyType) Method()`) |
| :--- | :--- | :--- |
| **操作对象** | 结构体的**副本** | 结构体的**原始实例** |
| **修改能力** | ❌ **不能**修改原始结构体 | ✅ **可以**修改原始结构体 |
| **性能开销** | 存在数据拷贝,对大结构体开销较大 | 无拷贝开销,仅传递指针(内存地址) |
| **NIL 值** | 不能用于 `nil` 接收者 | 可以为 `nil` 接收者定义方法(需在方法内做 `nil` 检查) |
| **调用** | Go 编译器会自动进行值和指针的转换,调用方便 | Go 编译器会自动进行值和指针的转换,调用方便 |
**[补充知识点] 最佳实践:如何选择接收者类型?**
1. **首选指针接收者**
- **修改状态**:当方法需要修改接收者的字段时,必须使用指针。
- **性能**:为避免大结构体的复制开销,即使不修改状态,也推荐使用指针。
- **一致性**:如果一个类型已经有了指针接收者的方法,那么其他方法也应保持一致,使用指针接收者,以避免混淆。
2. **何时使用值接收者**
- **不可变性**:当你想保证该方法不会修改接收者时。
- **小结构体**:对于非常小的、不包含指针或 slice 等引用类型的结构体(如 `type Point struct{X, Y int}`),值接收者的拷贝开销可以忽略不计。
- **并发安全**:创建副本可以天然地避免在并发环境下对共享数据的意外修改(但通常有更好的并发控制方法)。
```go
package main
import "fmt"
// Rectangle 定义了矩形结构体
type Rectangle struct {
width float64
height float64
}
// Area 是一个值接收者方法
// 它在 Rectangle 的副本上操作,因此是安全的,不会修改原始矩形。
func (r Rectangle) Area() float64 {
return r.width * r.height
}
// Scale 是一个指针接收者方法
// 它需要修改矩形的尺寸,所以必须使用指针接收者。
func (r *Rectangle) Scale(factor float64) {
r.width *= factor
r.height *= factor
}
func main() {
fmt.Println("--- 结构体方法示例 ---")
rect := Rectangle{width: 10, height: 5}
fmt.Printf("原始矩形: %+v, 面积: %.2f\n", rect, rect.Area())
// 调用指针接收者方法。Go 自动将 rect 转换为 &rect。
rect.Scale(2)
fmt.Printf("缩放后矩形: %+v, 面积: %.2f\n", rect, rect.Area())
// 指针类型也可以调用值接收者方法
rectPtr := &Rectangle{width: 3, height: 4}
// Go 自动将 rectPtr 解引用为 (*rectPtr) 来调用 Area()。
fmt.Printf("指针矩形: %+v, 面积: %.2f\n", *rectPtr, rectPtr.Area())
}
```
# 4. 结构体嵌套与组合
Go 语言通过结构体嵌套(或称嵌入)来实现**组合**,这是 Go `推荐`的代替传统面向对象中“继承”的设计模式。
## 4.1 显式嵌套
字段名和字段类型都明确给出。
```go
type Address struct {
City string
Country string
}
type Employee struct {
Name string
HomeAddress Address // 显式嵌套
}
// 访问: emp.HomeAddress.City
```
## 4.2 匿名嵌套(嵌入)
只给出字段类型,字段名默认为类型名。这会触发“字段提升”,使嵌套结构体的字段可以直接通过外部结构体访问。
```go
// [代码规范化]:这是一个完整的、可运行的示例,展示了显式和匿名嵌套。
package main
import "fmt"
// Address 结构体
type Address struct {
City string
Country string
}
// Contact 结构体,将作为匿名嵌套字段
type Contact struct {
Phone string
Email string
}
// Employee 结构体,组合了 Address 和 Contact
type Employee struct {
Name string
Age int
Addr Address // 显式嵌套,有字段名 Addr
Contact // 匿名嵌套(嵌入),字段被提升
}
func main() {
fmt.Println("--- 结构体嵌套与组合示例 ---")
emp := Employee{
Name: "王工程师",
Age: 35,
Addr: Address{
City: "深圳",
Country: "中国",
},
Contact: Contact{
Phone: "13800138000",
Email: "wang@example.com",
},
}
// 访问显式嵌套字段
fmt.Printf("地址: %s, %s\n", emp.Addr.City, emp.Addr.Country)
// 访问匿名嵌套(提升后)的字段
// 可以直接访问,就像它们是 Employee 的字段一样
fmt.Printf("联系方式: 电话 - %s, 邮箱 - %s\n", emp.Phone, emp.Email)
// 也可以通过类型名访问原始的匿名嵌套结构体
fmt.Printf("通过类型名访问邮箱: %s\n", emp.Contact.Email)
}
```
# 5. 匿名结构体 (Anonymous Structs)
匿名结构体是一种没有显式定义名称的结构体,非常适合用于那些只需要临时使用一次的数据结构,例如函数参数、返回值或测试数据。
```go
// [代码规范化]:这是一个完整的、可运行的示例,整合了匿名结构体的多种应用场景。
package main
import "fmt"
// getUserInfo 使用匿名结构体作为返回值,封装多个相关值。
func getUserInfo() struct {
ID int
Name string
} {
return struct {
ID int
Name string
}{
ID: 101,
Name: "临时用户",
}
}
func main() {
fmt.Println("--- 匿名结构体应用场景 ---")
// 场景1: 临时数据存储
// 在函数内部定义一个临时的、一次性的数据结构。
config := struct {
Host string
Port int
Enabled bool
}{
Host: "localhost",
Port: 8080,
Enabled: true,
}
fmt.Printf("临时配置: %+v\n\n", config)
// 场景2: 匿名结构体切片,常用于表格驱动测试
tests := []struct {
input int
expected string
}{
{input: 1, expected: "奇数"},
{input: 2, expected: "偶数"},
{input: -1, expected: "奇数"},
}
fmt.Println("表格驱动测试数据:")
for _, tc := range tests {
// 模拟测试逻辑
result := "偶数"
if tc.input%2 != 0 {
result = "奇数"
}
fmt.Printf(" 输入: %d, 期望: %s, 结果: %s, 通过: %t\n",
tc.input, tc.expected, result, result == tc.expected)
}
fmt.Println()
// 场景3: 作为函数返回值
user := getUserInfo()
fmt.Printf("从函数获取的用户信息: ID=%d, Name=%s\n", user.ID, user.Name)
}
```
# 6. 结构体字段的可见性 (Visibility)
Go语言使用大小写规则来控制可见性(导出性),而非 `public``private` 关键字。
- **大写开头**:导出的(Public)。结构体名或字段名如果以大写字母开头,则可以被包外的代码访问。
- **小写开头**:未导出的(Private)。结构体名或字段名如果以小写字母开头,则只能在定义它的包内部访问。
这个规则同样适用于函数、方法、常量和变量。
```go
// [代码规范化]:这是一个完整的、可运行的示例,用于解释可见性规则。
// 注意:真正的包外访问需要在不同的目录和文件中演示。
// 此处我们通过注释来解释这一概念。
package main
import "fmt"
// mypkg/models.go (模拟在一个名为 mypkg 的包中)
// ----------------------------------------------------
// User 是一个导出的结构体,因为它以大写字母'U'开头。
type User struct {
// ID 是一个导出的字段,因为'I'是大写的。包外代码可以访问 user.ID。
ID int
// email 是一个未导出的字段,因为'e'是小写的。包外代码无法访问 user.email。
email string
}
// NewUser 是一个导出的构造函数,用于创建 User 实例。
// 这是封装内部字段(如 email)的常用模式。
func NewUser(id int, email string) *User {
// 在包内部,我们可以访问所有字段,包括小写的 email。
return &User{
ID: id,
email: email,
}
}
// Email 方法是导出的,它提供了一种间接访问内部字段 email 的方式。
func (u *User) Email() string {
return u.email
}
// main.go (模拟在另一个包中)
// ----------------------------------------------------
func main() {
fmt.Println("--- 结构体可见性示例 ---")
// 我们可以调用 NewUser,因为它被导出了。
user := NewUser(1, "test@example.com")
// 我们可以直接访问导出的字段 ID。
fmt.Printf("用户ID (可访问): %d\n", user.ID)
// 以下代码如果位于不同的包中,将会导致编译错误:
// fmt.Println(user.email) // Error: user.email is not exported
// 我们只能通过导出的方法来访问它。
fmt.Printf("用户邮箱 (通过方法访问): %s\n", user.Email())
}
```
# 7. 结构体标签 (Struct Tags)
结构体标签是附加到结构体字段上的元数据字符串,在运行时可以通过反射读取。它们是 Go 语言实现与外部系统(如 JSON、XML、数据库 ORM)交互的关键机制。
标签的格式为:`\`` `key1:"value1" key2:"value2"` `` `
```go
// [代码规范化]:这是一个完整的、可运行的示例,展示了 JSON 标签的用法。
package main
import (
"encoding/json"
"fmt"
)
// Profile 定义了用户配置信息
type Profile struct {
// `json:"username"`: 定义了此字段在 JSON 中的键名。
Username string `json:"username"`
// `json:"-"`: 表示在 JSON 序列化/反序列化时忽略此字段。
Password string `json:"-"`
// `json:"age,omitempty"`: omitempty 表示如果字段值为零值(int 的 0),则在序列化时省略该字段。
Age int `json:"age,omitempty"`
// `json:"email"`: 正常映射。
Email string `json:"email"`
}
func main() {
fmt.Println("--- 结构体标签与 JSON 序列化示例 ---")
// 创建一个 Profile 实例
p1 := Profile{
Username: "gopher",
Password: "secret-password", // 这个字段将被忽略
Age: 0, // 这个字段因为 omitempty 和零值,将被省略
Email: "gopher@golang.org",
}
// 将结构体序列化为 JSON
jsonData, err := json.MarshalIndent(p1, "", " ") // MarshalIndent 用于格式化输出
if err != nil {
fmt.Println("JSON 序列化失败:", err)
return
}
// 输出结果,注意 Password 和 Age 字段都没有出现
fmt.Println("序列化后的 JSON:")
fmt.Println(string(jsonData))
}
```
# 8. 结构体与内存布局
Go 编译器会为了性能对结构体字段进行**内存对齐**。这意味着字段在内存中的顺序可能与你定义的顺序不同,并且可能会插入填充(padding)字节,以确保每个字段都从一个能被其大小整除的地址开始。
这对于需要与 C 库交互或进行底层性能优化的场景非常重要。
```go
// [代码规范化]:这是一个完整的、可运行的示例,演示了内存对齐的影响。
package main
import (
"fmt"
"unsafe"
)
func main() {
fmt.Println("--- 结构体内存布局示例 ---")
// 在 64 位系统上:
// bool (1字节), float64 (8字节), int16 (2字节)
type S1 struct {
b bool // 1 字节
f float64 // 8 字节
i int16 // 2 字节
}
// 内存布局可能像这样:
// b (1 byte) | padding (7 bytes) | f (8 bytes) | i (2 bytes) | padding (6 bytes)
// 总大小会为了对齐到最大字段(float64, 8字节)的倍数而变为 24 字节。
s1 := S1{}
fmt.Printf("S1 - 非优化布局: 大小 = %d, 对齐 = %d\n", unsafe.Sizeof(s1), unsafe.Alignof(s1))
// 通过重新排序字段,可以减少内存占用
type S2 struct {
f float64 // 8 字节
i int16 // 2 字节
b bool // 1 字节
}
// 内存布局可能像这样:
// f (8 bytes) | i (2 bytes) | b (1 byte) | padding (5 bytes)
// 总大小会为了对齐到最大字段(float64, 8字节)的倍数而变为 16 字节。
s2 := S2{}
fmt.Printf("S2 - 优化后布局: 大小 = %d, 对齐 = %d\n", unsafe.Sizeof(s2), unsafe.Alignof(s2))
fmt.Println("\n结论:合理安排字段顺序可以节省内存空间。")
}
```
+327
View File
@@ -0,0 +1,327 @@
# 一、核心概念:`error` vs. `panic`
在 Go 语言中,错误处理是一种设计哲学。它摒弃了其他语言中常见的 `try-catch` 异常机制,而是将 **错误(error** 作为一种普通的值来对待,鼓励开发者进行显式、清晰的处理。
| 特性 | `error` (错误) | `panic` (恐慌) |
| :------- | :------------------------ | :---------------------------------------------- |
| **定义** | 可预期的、业务逻辑的一部分 | 不可预期的、程序级的严重问题 |
| **场景** | 文件不存在、网络超时、数据库连接失败、用户输入无效 | 数组越界、空指针引用、并发访问 `map` 时的竞态条件 |
| **处理方式** | 作为函数的多返回值之一,必须显式检查和处理 | 中断当前函数的执行,并沿着调用栈向上传播,除非被 `recover` 捕获,否则将导致程序崩溃 |
| **目的** | 保证程序的健壮性和稳定性 | 快速失败,暴露程序中严重的、不应存在的 Bug |
**核心思想**:常规问题用 `error`,致命问题用 `panic`。**不要用 `panic` 来处理普通的错误**。
# 二、基本错误处理模式
Go 中最常见、最基础的错误处理模式是:函数在返回结果的同时,返回一个 `error` 类型的值。如果操作成功,`error``nil`;如果失败,`error` 则包含具体的错误信息。
```go
package main
import (
"fmt"
"os"
)
// 演示基本的错误处理流程
func main() {
// 尝试打开一个可能不存在的文件
file, err := os.Open("non-existent-file.txt")
// 黄金法则:拿到 error 后,立即检查!
if err != nil {
// 根据场景选择不同的处理策略
// 策略1:记录日志并优雅终止程序。这是最常见的做法。
fmt.Printf("错误:打开文件失败,详情: %v\n", err)
// 可以在此执行一些清理操作
return
// 策略2:如果错误不影响核心流程,可以只打印警告
// fmt.Printf("警告:一个可选配置文件加载失败, %v\n", err)
// 策略3:将错误包装后向上层调用者传递 (在非 main 函数中)
// return fmt.Errorf("初始化模块失败: %w", err)
}
// 确保资源在函数退出前被释放,即使后续代码发生 panic
defer file.Close()
// --- 仅在 err == nil 时,才会执行到这里 ---
fmt.Printf("文件 '%s' 打开成功。\n", file.Name())
// ... 接下来可以安全地使用 file 对象进行读写操作 ...
}
```
# 三、创建与包装错误
Go 提供了灵活的方式来创建和组合错误,以便提供更丰富的上下文信息。
## (一) `errors.New`:创建简单的静态错误
适用于创建不包含动态信息的、固定的错误信息。
```go
// [补充知识点] 这种在包级别定义的错误变量,被称为“哨兵错误”(Sentinel Error)。
// 调用者可以使用 errors.Is() 来判断是否是这种特定错误。
var ErrInvalidPort = errors.New("端口号无效")
```
## (二) `fmt.Errorf`:创建格式化的动态错误
当错误信息需要包含变量或动态内容时,使用 `fmt.Errorf`
```go
port := 80000
err := fmt.Errorf("端口 %d 超出有效范围 (1-65535)", port)
```
## (三) `fmt.Errorf` 与 `%w`:包装错误 (Wrapping)
这是现代 Go 错误处理的关键。使用 `%w` 动词可以将一个底层错误“包装”起来,形成一个错误链。这样做可以保留原始错误信息,便于上层代码进行检查和定位。
**[代码示例规范化]**
以下是一个综合了创建、包装和传递错误的完整示例:
```go
package main
import (
"errors"
"fmt"
)
// 模拟数据库连接层
func connectDB(host string, port int) error {
if host == "" {
// 使用 errors.New 创建一个简单的错误
return errors.New("数据库主机名不能为空")
}
if port <= 0 {
// 使用 fmt.Errorf 创建包含动态信息的错误
return fmt.Errorf("数据库端口 %d 无效", port)
}
fmt.Printf("正在尝试连接 %s:%d...\n", host, port)
// 模拟连接失败
return errors.New("网络超时")
}
// 模拟业务服务层
func startService() error {
err := connectDB("localhost", 3306)
if err != nil {
// 使用 %w 包装底层错误,添加上层上下文信息
// 这创建了一个错误链,保留了原始的“网络超时”错误
return fmt.Errorf("启动服务失败: %w", err)
}
return nil
}
func main() {
err := startService()
if err != nil {
// 输出的错误信息包含了从内到外的完整上下文
fmt.Printf("程序启动异常: %v\n", err)
// 输出: 程序启动异常: 启动服务失败: 网络超时
}
}
```
# 四、检查与解包错误
错误包装后,我们需要有效的方法来检查错误链中的特定错误。Go 1.13 引入了 `errors.Is``errors.As`
- **`errors.Is(err, target error) bool`**: 判断 `err` 的错误链中是否**包含**目标 `target` 错误。常用于检查是否为某个特定的哨兵错误(如 `io.EOF`)。
- **`errors.As(err, target interface{}) bool`**: 判断 `err` 的错误链中是否存在**某种类型**的错误,如果存在,则将该错误赋值给 `target`。常用于处理自定义错误类型。
下面这个例子演示了如何对一个被层层包装的错误进行精确的检查。
```go
package main
import (
"errors"
"fmt"
"os"
)
// 自定义一个文件操作错误类型
type FileError struct {
Path string // 文件路径
Op string // 操作类型
Err error // 底层错误
}
func (e *FileError) Error() string {
return fmt.Sprintf("文件操作失败 -> 路径: %s, 操作: %s, 原因: %v", e.Path, e.Op, e.Err)
}
// 这是底层错误,我们将使用 errors.Is 来检查它
var ErrPermissionDenied = errors.New("权限不足")
// 模拟一个深层次的读取操作
func readConfig() error {
// 模拟因为权限问题导致失败
return ErrPermissionDenied
}
// 包装操作
func loadConfig(path string) error {
err := readConfig()
if err != nil {
// 将底层错误包装进我们的自定义错误类型中
return &FileError{Path: path, Op: "加载配置", Err: err}
}
return nil
}
func main() {
err := loadConfig("/etc/app.conf")
if err != nil {
fmt.Printf("发生错误: %v\n\n", err)
// 使用 errors.Is() 检查错误链中是否包含“权限不足”这个哨兵错误
if errors.Is(err, ErrPermissionDenied) {
fmt.Println("检查结果: 这是一个权限问题。请检查文件权限。")
} else {
fmt.Println("检查结果: 不是权限问题。")
}
// 使用 errors.As() 尝试从错误链中提取 FileError 类型的信息
var fileErr *FileError
if errors.As(err, &fileErr) {
fmt.Println("检查结果: 这是一个文件错误。")
fmt.Printf(" - 操作路径: %s\n", fileErr.Path)
fmt.Printf(" - 操作类型: %s\n", fileErr.Op)
} else {
// 如果错误链中没有 FileError 类型,os.PathError 也算是一种文件错误!
// 这展示了 As 的灵活性,它可以检查任何实现了 error 接口的类型。
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("检查结果: 这是一个标准的路径错误: %v\n", pathErr)
}
}
}
}
```
# 五、自定义错误类型
通过定义自己的错误类型(实现 `error` 接口),我们可以携带更丰富的业务信息,如错误码、详细上下文等。
```go
package main
import (
"errors"
"fmt"
)
// 定义一个包含业务代码和信息的错误结构体
type BusinessError struct {
Code int // 业务错误码
Message string // 用户可读的错误信息
}
// 实现 error 接口
func (e *BusinessError) Error() string {
return fmt.Sprintf("业务异常 -> Code: %d, Message: %s", e.Code, e.Message)
}
// 模拟用户验证
func checkUserAge(age int) error {
if age < 18 {
// 返回自定义的错误类型实例
return &BusinessError{
Code: 4003,
Message: "用户未成年",
}
}
return nil
}
func main() {
err := checkUserAge(16)
if err != nil {
fmt.Printf("原始错误: %v\n", err)
var bizErr *BusinessError
// 使用 errors.As 是检查和转换自定义错误类型的最佳方式
if errors.As(err, &bizErr) {
fmt.Println("检测到业务错误,可以进行特殊处理:")
fmt.Printf("错误码: %d\n", bizErr.Code)
fmt.Printf("信息: %s\n", bizErr.Message)
// 例如,根据 bizErr.Code 返回不同的 HTTP 状态码
} else {
fmt.Println("检测到非业务类型的其他系统错误。")
}
}
}
```
# 六、panic` 与 `recover`
`panic` 用于表示程序无法继续运行的严重错误。`recover` 是一个内置函数,可以捕获并处理 `panic`,避免程序崩溃。
- `recover` 只有在 `defer` 修饰的函数中调用时才有效。
- `recover` 的主要应用场景是在一个 goroutine 的入口处,防止该 goroutine 的 `panic` 导致整个应用程序崩溃,特别是在网络服务器等需要长时间运行的程序中。
```go
package main
import (
"fmt"
)
// 模拟一个可能会发生严重错误的函数
func criticalOperation(data interface{}) {
fmt.Println("开始执行关键操作...")
if data == nil {
// 遇到一个无法恢复的逻辑错误,比如一个必须存在的对象是 nil
panic("关键数据为 nil,操作无法继续!")
}
// 将 data 转换为 string (如果 data 不是 string 类型,这里也会 panic)
str := data.(string)
fmt.Printf("操作成功,数据: %s\n", str)
}
// 一个安全的执行器,它会捕获内部的 panic
func safeExecutor(f func(interface{}), data interface{}) {
// defer 确保在函数退出前执行,即使发生 panic
defer func() {
// recover() 用于捕获 panic
if r := recover(); r != nil {
fmt.Printf("!!! 捕获到 Panic !!!\n")
fmt.Printf(" - 恢复执行,避免程序崩溃。\n")
fmt.Printf(" - Panic 信息: %v\n", r)
}
}()
fmt.Println("--- 安全执行器启动 ---")
f(data)
fmt.Println("--- 安全执行器结束 ---")
}
func main() {
// 案例1:触发 panic
safeExecutor(criticalOperation, nil)
fmt.Println("\n--------------------------\n")
// 案例2:不触发 panic
safeExecutor(criticalOperation, "正常数据")
fmt.Println("\n程序已从 panic 中恢复,并继续执行到结束。")
}
```
# 七、错误处理最佳实践
1. **绝不忽略错误**:永远不要使用 `_` 来丢弃一个 `error` 返回值,除非你百分之百确定调用不会失败。始终显式地检查它。
2. **提供上下文信息**:当错误向上传递时,使用 `fmt.Errorf``%w` 进行包装,为错误添加上下文。`原始错误: 连接超时` 不如 `上层错误: 无法连接数据库: 连接超时` 有用。
3. **只处理一次错误**:一个错误应该在一个层级上被完整处理。避免在一个地方记录日志,然后又返回 `error` 让上层再次记录,这会导致日志重复和逻辑混乱。要么处理它(例如重试、记录并返回 `nil`),要么包装它并返回。
4. **使用 `errors.Is` 和 `errors.As`**:优先使用这两个函数来检查和解包错误,它们对错误链友好,比传统的类型断言或字符串比较更健壮。
5. **为你的包定义哨兵错误和自定义错误类型**:如果你的函数可能返回特定的、可被调用者区分的错误,请定义并导出它们。
6. **使用 `defer` 确保资源释放**:打开文件、建立网络连接、获取锁等操作后,立即使用 `defer` 编写资源释放语句,确保无论函数是正常返回还是因错误退出,资源都能被清理。
@@ -0,0 +1,248 @@
# 环境准备
## 下载安装
下载地址: https://go.dev
双击“下一步”安装,注意安装位置尽可能不放 C 盘,可以建立自己的环境目录。
安装完成后在 cmd 控制台输入 `go version` 查看 Go 版本,检测是否安装成功。
环境变量在新版本后不需要过多额外设置,但需注意用户变量中默认有 GOPATH 的设置,如果不希望放在默认用户目录下,需进行修改。
`GOPATH` 即为存储 Go 语言项目的路径,包含 src(已不强制要求)、pkg、bin 三个目录。
打开 cmd 控制台,输入 `go env` 查看 Go 的环境变量配置。
依次在 cmd 控制台设置代理和 `GO111MODULE`
```shell
go env -w GOPROXY=https://goproxy.cn,direct
go env -w GO111MODULE=auto
```
## 关于 GOPATH
在 Go 语言开发中,`GOPATH` 是一个环境变量,用于定义 Go 代码的工作区。传统的 Go 项目结构基于 `GOPATH` 下的三个经典目录,分别是:
1. src 存放 Go 源代码文件。按照惯例,源代码文件组织成以包为单位的目录,例如:`github.com/user/project`
2. pkg 存放编译后的包文件(以 `.a` 结尾)。这些文件通常用于加速编译。
3. bin 存放编译生成的可执行文件。执行 `go install` 时,生成的二进制文件会放在 bin 目录中,方便直接运行。
### 示例目录结构
```
GOPATH
├── src // 源代码目录,存放你的 Go 项目代码
├── bin // 可执行文件目录,存放编译后的可执行文件
└── pkg // 已编译的包目录
├── linux_amd64 // 针对特定平台和架构编译的包文件
│ ├── foo.a // 编译后的 foo 包文件
│ └── bar.a // 编译后的 bar 包文件
└── mod // 在 Go Modules 模式下使用,存放模块缓存
```
### 为什么现在不再注重 GOPATH?
自从 Go 1.11 引入了 **Go Modules** 后,`GOPATH` 不再是项目管理的核心部分,以下是原因:
1. 模块化依赖管理
Go Modules 使用 `go.mod` 文件来管理依赖和版本,解决了 `GOPATH` 下多个项目共享依赖库时的冲突问题,避免了“依赖地狱”。
2. 无需固定目录
使用 Go Modules 后,项目可以放在任意目录下,而不再需要存放在 `GOPATH` 的 src 目录中,增加了开发的灵活性。
3. 版本控制
Go Modules 原生支持版本控制和依赖管理,确保项目中使用的是正确版本的依赖,而 `GOPATH` 无法提供这种能力。
4. 更简单的构建
通过 Go Modules,开发者可以直接运行 `go build``go run`,无需考虑是否在 `GOPATH` 内,提升了开发体验。
# 开发工具
采用 VsCode,如果写过 Java 可以使用 GoLand,会更顺手,一样的 UI。
1. 安装 VsCode 软件,在插件市场安装 GO 插件。
2. VsCode 软件的`settings.json` 文件中新增以下配置, 设置 `go tools` 的全局安装目录。
- `"go.toolsGopath": "D:\\MyGo\\go-tools"`
3. 安装扩展:快捷键 `ctrl+shift+p`,输入 `Go install`,选择 `Install/Update Tools`,勾选所有插件(插件显示可能会反应慢一些,稍等。)进行安装。
4. 等待安装完成。
# Hello World
创建文件:`hello.go`
```go
package main
import "fmt"
func main() {
fmt.Println("Hello World!")
}
```
## 1. package(声明包)
定义:Go 源文件必须以 `package` 开头,声明其所属的包。
1. 包名与文件夹一一对应,但可以不同。
2. 一个程序必须有且仅有一个 `main` 包,作为入口包。
3.`main` 包的程序无法生成可执行文件。
## 2. import(导入包)
格式: `import "包名"` 或用括号引入多个包:
```go
import (
"name1"
"name2"
)
```
> 注意:
> 只能导入已使用的包,未使用的包会导致编译错误。
> `fmt` 是 Go 标准库,用于格式化输入输出。
## 3. main 函数
Go 程序的入口函数,必须声明在 `main` 包中,且只能有一个。
其他包中不能定义 `main` 函数。
```go
func 函数名 (参数列表) (返回值列表) {
函数体
}
```
- 参数列表:`变量名 类型` 组合,如 `a int, b string`
- 返回值列表:支持多个返回值,用 `return` 返回。
- 函数体左大括号 `{` 必须与函数声明在同一行。
## 4. 打印 Hello World
`Println``fmt` 包的函数,用于格式化输出,自动换行。
点号 `.` 表示调用 `fmt` 包的函数。
结尾无需分号 `;`Go 编译器会自动处理。
# 编译和运行
Go 是编译型静态语言,在运行程序之前需将代码编译为二进制可执行文件。Go 提供两种主要命令用于编译和运行:`go build``go run`
| 命令 | 功能 | 生成文件 | 适用场景 |
| ---------- | ------------------ | ------------- | ----------- |
| `go build` | 编译 Go 程序为可执行文件 | 生成 `.exe` 文件 | 用于生产环境或打包分发 |
| `go run` | 编译后立即运行程序,不生成可执行文件 | 不生成文件,仅临时编译运行 | 用于调试、快速测试 |
## go build 命令
```go
go build fileName
```
`fileName`:Go 源文件名,可以是一个或多个,多个文件名用空格分隔;也可省略,省略时默认为当前目录。
参数不为空:
- 如果 `fileName` 属于 `main` 包,生成与第一个 `fileName` 同名的可执行文件(如 `abc.go` 生成 `abc.exe`)。
- 如果 `fileName` 不属于 `main` 包,只进行语法检查,不生成可执行文件。
参数为空:
- 如果当前目录包含 `main` 包,生成与目录同名的 `.exe` 文件(如 `hello` 目录下生成 `hello.exe`)。
- 如果目录中无 `main` 包,仅进行语法检查。
```go
go build .\hello.go
```
Windows 系统:当前目录使用 `.\` 表示。
类 Unix 系统:当前目录使用 `./` 表示。
# 常用 Print 方式
## 1. Print
将内容**直接**输出到控制台。
不接受任何格式化,占位符无效。
等价于对每一个操作数应用 `%v`
示例:
```go
fmt.Print("Hello, World!")
```
输出:`Hello, World!`
## 2. Println
输出内容后自动**换行**。
同样不支持格式化,占位符无效。
示例:
```go
fmt.Println("Hello,", "World!")
fmt.Println("Hello,", "World!")
```
输出:
```
Hello, World!
Hello, World!
```
## 3. Printf
支持**格式化**输出,可按照指定的格式输出数据。
需要提供格式化占位符。
语法:
```go
fmt.Printf(format, values...)
```
示例:
```go
var a, b, c = 1, 2, 3
fmt.Printf("a = %d, b = %d, c = %d\n", a, b, c)
```
输出:
```
a = 1, b = 2, c = 3
```
> `\n` 为占位符。
## 常见占位符
| 占位符 | 描述 |
| ------------- | ------------------------------------- |
| `%v` | 以默认格式打印变量的值(适用于大多数类型) |
| `%T` | 打印变量的类型(类型名) |
| `%s` | 输出字符串 |
| `%t` | 输出布尔值(`true``false` |
| `%d` | 打印整数的十进制表示 |
| `%b` | 打印整数的二进制表示 |
| `%o` / `%#o` | 八进制,不带零 / 带零(0o,零加小写字母 o) |
| `%x` / `%#x` | 小写十六进制 / 带 `0x` 前缀的小写十六进制 |
| `%X` / `%#X` | 大写十六进制 / 带 `0X` 前缀的大写十六进制 |
| `%f` / `%.3f` | 浮点数,默认精度 6 位小数 / 保留 3 位小数,位数可控 |
| `%e` / `%.3e` | 科学计数法,默认精度 6 位小数 / 保留 3 位小数 |
| `%U` / `%#U` | Unicode 字符 / 带字符的 Unicode,用 `rune` 声明 |
| `%p` | 打印指针的地址,带 `0x` 前缀,变量前加 `&` 取指针 |
| `%q` | 带双引号的字符串,字符串内的引号用转义符 |
| `%c` | 打印一个 Unicode 字符(根据整数值打印对应的字符) |
@@ -0,0 +1,919 @@
# 注释
Go 语言的注释主要分成两类,分别是**单行**注释和**多行**注释。
```go
package main
import "fmt"
/*
多行注释。
*/
func main() {
// 单行注释。
fmt.Printf("单行注释。")
/*
多行注释
*/
fmt.Println("多行注释。")
}
```
> 发现一个现象,写在函数上不会被格式化,在代码中会被退格格式化。
# 变量
## 声明
```go
package main
import "fmt"
func main() {
// 1️⃣ 变量声明(包含默认值)
var name string = "田卓"
var age int = 27
var isGood bool = true
var myAge int // 默认值 0
fmt.Println("默认值示例:")
fmt.Println("name:", name)
fmt.Println("age:", age)
fmt.Println("isGood:", isGood)
fmt.Println("myAge (未赋值,默认值):", myAge)
fmt.Println()
// 2️⃣ 批量声明变量(默认值)
var (
name1 string
age1 int
)
fmt.Println("批量声明:")
fmt.Println("name1:", name1) // 默认 ""
age1 = 20
fmt.Println("age1:", age1) // 赋值后 20
fmt.Println()
// 3️⃣ 变量自动类型推导(语法糖 :=)
name2 := "田秉衡" // Go 自动推导为 string
age2 := 2 // 自动推导为 int
height := 8 // 自动推导为 int
weight := 28 // 自动推导为 int
fmt.Println("类型推导:")
fmt.Println("name2:", name2)
fmt.Println("age2:", age2)
fmt.Println("height:", height)
fmt.Println("weight:", weight)
fmt.Println()
// 4️⃣ 变量交换
nameA := "田卓"
nameB := "田秉衡"
fmt.Println("交换前:", nameA, nameB)
nameA, nameB = nameB, nameA
fmt.Println("交换后:", nameA, nameB)
fmt.Println()
// 5️⃣ 多返回值赋值(使用 `_` 忽略不需要的值)
result, _ := test() // 只取第一个返回值,忽略第二个
fmt.Println("多返回值赋值(忽略不需要的值):", result)
}
// 定义一个返回两个整数的函数
func test() (int, int) {
return 200, 200
}
```
## 作用域
```go
package main
import "fmt"
// 1️⃣ 全局变量(作用域:整个 package)
var globalVar string = "全局变量"
func main() {
// 2️⃣ 局部变量(作用域:main 函数内)
var localVar string = "局部变量"
fmt.Println("🌍 全局变量:", globalVar)
fmt.Println("🔹 局部变量:", localVar)
// 3️⃣ 代码块作用域
{
blockVar := "代码块变量"
fmt.Println("📦 代码块变量:", blockVar)
}
// ❌ 代码块变量超出作用域,下面的代码会报错
// fmt.Println(blockVar) // ❌ 编译错误:未定义 blockVar
// 4️⃣ 变量遮蔽(局部变量遮蔽全局变量)
globalVar := "局部遮蔽全局"
fmt.Println("🚧 遮蔽后的 globalVar:", globalVar)
// 5️⃣ 变量交换
a, b := "变量A", "变量B"
fmt.Println("🔄 交换前:", a, b)
a, b = b, a
fmt.Println("🔄 交换后:", a, b)
// 6️⃣ 函数参数作用域
printVar("函数参数")
// 7️⃣ 访问真正的全局变量(使用 package 级别变量)
fmt.Println("🌍 真实的全局变量仍然是:", getGlobalVar())
}
// 6️⃣ 函数参数作用域
func printVar(param string) {
fmt.Println("🎯 函数参数作用域:", param)
}
// 7️⃣ 访问全局变量的辅助函数
func getGlobalVar() string {
return globalVar
}
```
# 常量
```go
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
// 1️⃣ 基本常量声明
const URL1 string = "www.baidu.com"
fmt.Println("🌍 基本常量:", URL1)
// 2️⃣ 多常量声明
const URL2, URL3 string = "www.baidu1.com", "www.baidu2.com"
fmt.Println("🌍 多常量:", URL2, URL3)
// 3️⃣ 常量枚举(iota
const (
A = iota // 0
B // 1
C // 2
)
fmt.Println("🔢 常量 iota:", A, B, C)
// 4️⃣ `iota` 用法(位移计算)
const (
Flag1 = 1 << iota // 1 (2^0)
Flag2 // 2 (2^1)
Flag3 // 4 (2^2)
)
fmt.Println("🛠️ iota 位移:", Flag1, Flag2, Flag3)
// 5️⃣ 变量可修改,但常量不能修改
var myVar int = 100
fmt.Println("🔄 变量修改前:", myVar)
// 6️⃣ 变量地址修改(⚠️ 变量可以被 unsafe 修改,但常量不能)
myVarPtr := unsafe.Pointer(&myVar)
rv := reflect.NewAt(reflect.TypeOf(myVar), myVarPtr).Elem()
// 修改值
rv.SetInt(200)
fmt.Println("🔄 变量修改后:", myVar)
// 7️⃣ 常量的内存地址(不会被修改)
// ❌ 下面代码如果取消注释会报错:常量不能被修改
// const myConst int = 300
// fmt.Printf("🚫 常量内存地址: %p (常量不会被修改)\n", &myConst)
// rvConst := reflect.NewAt(reflect.TypeOf(myConst), unsafe.Pointer(&myConst)).Elem()
// rvConst.SetInt(500)
// fmt.Println("🚫 常量修改后:", myConst)
}
```
## iota 特殊常量
```go
package main
import "fmt"
func main() {
// 1️⃣ `iota` 基本递增
const (
a = iota // 0
b // 1 (继承 `a = iota`iota 递增)
c // 2
d = 0 // ❌ 这里手动赋值 d = 0,iota 仍然递增(跳过)
e // 0 (继承 d = 0,而不是 iota)
f = iota // 5 (iota 继续递增,从 `c = 2` 到 `iota = 5`)
g // 6
h = iota // 7 (手动写 `iota`,但值仍然递增)
)
fmt.Println("🔢 基本 iota:", a, b, c, d, e, f, g, h) // 0 1 2 0 0 5 6 7
// 2️⃣ `iota` 遇到手动赋值的影响
const (
i = iota // 0
j = 0 // ❌ 手动赋值 j = 0,但 iota 仍然递增
k = iota // 2 (iota 继续递增,从 `i = 0` 变为 `k = 2`)
)
fmt.Println("🎯 iota 断开影响:", i, j, k) // 0 0 2
}
```
# 数据类型
## 布尔类型 (bool)
```go
package main
import "fmt"
func main() {
// ✅ 布尔值只能是 true 或 false
var b1 bool = true
var b2 bool = false
// 🔹 %t 格式化布尔值
fmt.Printf("b1: %t, b2: %t\n", b1, b2)
// ✅ Go 规定:false = 0, true = 1
var num1, num2 int = 1, 2
if num1 < num2 {
fmt.Println("✅ 1 小于 2")
}
// ⚠️ 布尔类型默认值是 `false`
var b3 bool
fmt.Println("🔹 bool默认值:", b3) // 输出 false
}
```
- 布尔类型 `bool` 只能是 `true``false`
- 默认值是 `false`
- `if` 语句可以直接使用布尔表达式
## 整数类型 (int, uint)
```go
package main
import "fmt"
func main() {
// ✅ 无符号整数类型(uint
var u8 uint8 = 255
fmt.Printf("uint8: %v\n", u8)
// ✅ 有符号整数类型(int
var i8 int8 = -128
fmt.Printf("int8: %v\n", i8)
}
```
| 类型 | 描述 | 取值范围 |
| -------- | ---------- | ---------------------------------------------- |
| `uint8` | 无符号 8 位整型 | `0` - `255` |
| `uint16` | 无符号 16 位整型 | `0` - `65535` |
| `uint32` | 无符号 32 位整型 | `0` - `4294967295` |
| `uint64` | 无符号 64 位整型 | `0` - `18446744073709551615` |
| `int8` | 有符号 8 位整型 | `-128` - `127` |
| `int16` | 有符号 16 位整型 | `-32768` - `32767` |
| `int32` | 有符号 32 位整型 | `-2147483648` - `2147483647` |
| `int64` | 有符号 64 位整型 | `-9223372036854775808` - `9223372036854775807` |
- **`int` 类型** 在 `32-bit``64-bit` 平台上的大小可能不同
- **`uint` 是无符号整数,不能表示负数**
- **默认 `int``int64`64-bit 机器)**
## 浮点类型 (float32, float64)
```go
package main
import "fmt"
func main() {
// ✅ 浮点类型(默认 `float64`)
var f1 float64 = 3.141592653
var f2 float32 = 2.71
fmt.Printf("float64: %T, %.3f\n", f1, f1) // 保留3位小数
fmt.Printf("float32: %T, %.2f\n", f2, f2) // 保留2位小数
// ⚠️ 计算机存储浮点数时存在精度丢失
var num1 float32 = -123.0000901
var num2 float64 = -123.0000901
fmt.Println("float32:", num1)
fmt.Println("float64:", num2)
}
```
- `float64` **精度比 `float32` 高**
- **浮点数计算可能会有精度损失**
- **格式化输出 `%.2f` 控制小数位数**
## 特殊整数类型 (byte, rune, uintptr)
```go
package main
import (
"fmt"
"unsafe"
)
func main() {
// ✅ `byte` 是 `uint8` 的别名(常用于存储字符)
var b byte = 255
fmt.Printf("byte: %T, %v\n", b, b)
// ✅ `rune` 是 `int32` 的别名(用于存储 Unicode 字符)
var r rune = '魑'
fmt.Printf("rune: %T, %v, %c\n", r, r, r)
// ✅ `uintptr` 存储指针地址
var x int = 42
ptr := unsafe.Pointer(&x)
// 强制转换
uintPtr := uintptr(ptr)
fmt.Printf("Pointer: %v, Uintptr: %v\n", ptr, uintPtr)
}
```
- `byte` **等价于 `uint8`**,用于 **存储 ASCII 字符**
- `rune` **等价于 `int32`**,用于 **存储 Unicode 字符**
- `uintptr` **用于存储指针地址**
## 字符串类型 (string)
```go
package main
import "fmt"
func main() {
// ✅ 字符串类型
var str string = "Hello, World!"
fmt.Printf("string: %T, %s\n", str, str)
// ✅ 单引号是字符(rune),双引号是字符串(string)
v1 := 'A' // `rune`int32
v2 := "A" // `string`
fmt.Printf("rune: %T, %d\n", v1, v1) // 'A' 的 Unicode 编码值
fmt.Printf("string: %T, %s\n", v2, v2)
// ✅ Unicode 字符
v3 := '魑'
fmt.Printf("rune: %T, %d, %c\n", v3, v3, v3)
}
```
- **单引号 `'A'``rune`int32**
- **双引号 `"A"``string`**
- **字符本质上是 `int32`Unicode 码点)**
## 类型转换
```go
package main
import "fmt"
func main() {
// ✅ 浮点数转整数(丢失小数部分)
a := 5.9
b := int(a)
fmt.Printf("浮点转整型: %v\n", b) // 输出 5
// ✅ 整数转浮点数
var c int = 10
var d float64 = float64(c)
fmt.Printf("整数转浮点: %v\n", d) // 输出 10.000000
// ✅ `byte` 和 `rune` 互转
var ch rune = 'A'
fmt.Printf("字符 'A' 转 int: %v\n", int(ch)) // 65
// ✅ `string` 转 `byte`
str := "Go"
byteArr := []byte(str)
fmt.Printf("字符串转 byte: %v\n", byteArr) // [71 111]
}
```
- **转换 `float``int` 会丢失小数部分**
- **整型可以转换为 `float64`**
- **字符 (`rune`) 本质是 `int32`,可以直接转整数**
- **`string` 可以转换为 `byte[]`**
# 运算符全
## 算术运算符
| 运算符 | 描述 | 示例(a = 7, b = 3 | 结果 |
| ---- | ------------- | ---------------- | ------------------- |
| `+` | 加法 | `a + b` | `7 + 3 = 10` |
| `-` | 减法 | `a - b` | `7 - 3 = 4` |
| `*` | 乘法 | `a * b` | `7 * 3 = 21` |
| `/` | 除法 | `a / b` | `7 / 3 = 2`(整数除法取整) |
| `%` | 取模(求余数) | `a % b` | `7 % 3 = 1` |
| `++` | 自增(不支持 `++a` | `a++` | `a = a + 1` |
| `--` | 自减(不支持 `--a` | `a--` | `a = a - 1` |
```go
package main
import "fmt"
func main() {
a, b := 7, 3
fmt.Println("加法:", a+b)
fmt.Println("减法:", a-b)
fmt.Println("乘法:", a*b)
fmt.Println("除法:", a/b) // 结果是 2,整数除法取整
fmt.Println("取模:", a%b) // 余数 1
// 自增 & 自减
a++
fmt.Println("自增:", a) // 8
a--
fmt.Println("自减:", a) // 7
}
```
## 关系运算符
| 运算符 | 描述 | 示例(a = 21, b = 10 | 结果 |
| ---- | ---- | ------------------ | ------- |
| `>` | 大于 | `a > b` | `true` |
| `<` | 小于 | `a < b` | `false` |
| `>=` | 大于等于 | `a >= b` | `true` |
| `<=` | 小于等于 | `a <= b` | `false` |
| `==` | 等于 | `a == b` | `false` |
| `!=` | 不等于 | `a != b` | `true` |
```go
package main
import "fmt"
func main() {
a, b := 21, 10
fmt.Println(a > b) // true
fmt.Println(a >= b) // true
fmt.Println(a < b) // false
fmt.Println(a <= b) // false
fmt.Println(a == b) // false
fmt.Println(a != b) // true
}
```
## 逻辑运算符
```go
package main
import "fmt"
func main() {
a, b := true, false
fmt.Println("与 &&:", a && b) // false
fmt.Println("或 ||:", a || b) // true
fmt.Println("非 !:", !a) // false
}
```
## 位运算符
| 运算符 | 描述 | 示例(a = 60, b = 13) | 结果(二进制) | 结果(十进制) |
| ---- | ---- | ------------------ | ------------------------ | ---------------- |
| `&` | 按位与 | `a & b` | `0011 1100 & 0000 1101` | `0000 1100 = 12` |
| ` | ` | 按位或 | `a | b` |
| `^` | 按位异或 | `a ^ b` | `0011 1100 ^ 0000 1101` | `0011 0001 = 49` |
| `&^` | 按位清空 | `a &^ b` | `0011 1100 &^ 0000 1101` | `0011 0000 = 48` |
| `<<` | 左移 | `a << 2` | `1111 0000` | `240` |
| `>>` | 右移 | `a >> 2` | `0000 1111` | `15` |
```go
package main
import "fmt"
func main() {
a, b := 60, 13
fmt.Println("按位与 &:", a&b) // 12
fmt.Println("按位或 |:", a|b) // 61
fmt.Println("按位异或 ^:", a^b) // 49
fmt.Println("位清空 &^:", a&^b) // 48
fmt.Println("左移 <<:", a<<2) // 240
fmt.Println("右移 >>:", a>>2) // 15
}
```
## 赋值运算符
| 运算符 | 描述 | 示例 |
| ---- | ----- | ---------------------- |
| `=` | 赋值 | `a = 10` |
| `+=` | 加后赋值 | `a += 5``a = a + 5` |
| `-=` | 减后赋值 | `a -= 5``a = a - 5` |
| `*=` | 乘后赋值 | `a *= 5``a = a * 5` |
| `/=` | 除后赋值 | `a /= 5``a = a / 5` |
| `%=` | 取模后赋值 | `a %= 5``a = a % 5` |
```go
package main
import "fmt"
func main() {
a := 10
a += 5
fmt.Println("a += 5:", a) // 15
a *= 2
fmt.Println("a *= 2:", a) // 30
}
```
## 指针运算符
| 运算符 | 描述 | 示例 |
| --- | ------------- | ------------ |
| `&` | 取地址 | `ptr = &a` |
| `*` | 解引用(获取指针指向的值) | `val = *ptr` |
```go
package main
import "fmt"
func main() {
a := 10
ptr := &a // 获取变量 a 的地址
fmt.Println("a 的地址:", ptr)
fmt.Println("指针解引用:", *ptr) // 10
}
```
## 运算符优先级
**高****低**
1. `*` `/` `%` `<<` `>>` `&` `&^`
2. `+` `-` `|` `^`
3. `==` `!=` `<` `<=` `>` `>=`
4. `&&`
5. `||`
# 流程控制
## 顺序结构
**默认执行顺序**:代码从上到下依次执行,不需要特殊控制。
```go
package main
import "fmt"
func main() {
fmt.Println("步骤 1")
fmt.Println("步骤 2")
fmt.Println("步骤 3")
}
```
**结果**
```
步骤 1
步骤 2
步骤 3
```
## 选择结构(if 语句)
### 1)基本 if 语句
```go
package main
import "fmt"
func main() {
num := 15
if num > 10 {
fmt.Println("num > 10")
}
fmt.Println("main 结束")
}
```
### 2if-else 语句
```go
package main
import "fmt"
func main() {
score := 90
if score == 100 {
fmt.Println("满分")
} else {
fmt.Println("不是满分")
}
}
```
### 3if-else if-else 多条件判断
```go
package main
import "fmt"
func main() {
var score int
fmt.Println("请输入成绩:")
fmt.Scan(&score)
if score >= 90 && score <= 100 {
fmt.Println("A")
} else if score >= 80 {
fmt.Println("B")
} else if score >= 70 {
fmt.Println("C")
} else if score >= 60 {
fmt.Println("D")
} else if score < 0 || score > 100 {
fmt.Println("输入不合法")
} else {
fmt.Println("不及格")
}
}
```
## 选择结构(switch 语句)
### 1)基本 switch
```go
package main
import "fmt"
func main() {
score := 100
switch score {
case 100, 95, 91:
fmt.Println("A")
case 90:
fmt.Println("B")
case 80, 70, 60:
fmt.Println("C")
default:
fmt.Println("其他")
}
}
```
- `case` **支持多个匹配值**(如 `100, 95, 91`
- `default` **可选**
### 2switch 省略条件
```go
package main
import "fmt"
func main() {
switch {
case false:
fmt.Println("false")
case true:
fmt.Println("true")
default:
fmt.Println("其他")
}
}
```
- `switch` **默认匹配 `true`**,可以用作 `if-else if-else` 结构的替代。
### 3fallthrough 关键字
`fallthrough` 用于强制执行下一个 `case` 语句,即使条件不匹配。
```go
package main
import "fmt"
func main() {
a := false
switch a {
case false:
fmt.Println("1")
fallthrough
case true:
fmt.Println("2") // 强制执行
}
}
```
- **`fallthrough` 忽略 `case` 的匹配条件**
- **避免滥用**,否则可能产生意外行为
### 4)终止 fallthrough
```go
package main
import "fmt"
func main() {
a := false
switch a {
case false:
fmt.Println("1")
fallthrough
case true:
fmt.Println("2")
if !a {
break // 终止 `fallthrough`
}
fallthrough
default:
fmt.Println("3")
}
}
```
## 循环结构(for 语句)
### 1)基本 for 语句
```go
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
fmt.Println(i)
}
}
```
### 2)变形 for 语句
省略初始值 & 结束条件
```go
package main
import "fmt"
func main() {
i := 0
for i <= 5 {
fmt.Println(i)
i++
}
}
```
无限循环
```go
package main
import "fmt"
func main() {
for {
fmt.Println("死循环")
}
}
```
## 终止循环
### 1break 语句
用于**终止**整个循环。
```go
package main
import "fmt"
func main() {
for i := 1; i <= 10; i++ {
if i == 5 {
break
}
fmt.Println(i)
}
}
```
`i == 5` 时,`break` 终止循环,输出 `1 2 3 4`
### 2continue 语句
用于**跳过当前循环**,继续执行下一个循环。
```go
package main
import "fmt"
func main() {
for i := 1; i <= 10; i++ {
if i == 5 {
continue // 跳过 5
}
fmt.Println(i)
}
}
```
输出 `1 2 3 4 6 7 8 9 10``5` 被跳过)
## goto 语句
用于**无条件跳转**,但容易造成代码混乱,不建议使用。
`goto` 只能跳转到**同一函数**内的标签。
```go
package main
import "fmt"
func main() {
fmt.Println("开始")
goto END // 跳转到 END
fmt.Println("不会执行") // 这行代码被跳过
END:
fmt.Println("结束")
}
```
**输出**
```
开始
结束
```
@@ -0,0 +1,293 @@
# strconv
bool - 字符串 ,“true” = 转换 true
数字 - 字符串, “3.99” / 转换为数字才可以进行计算
```go
package main
import (
"fmt"
"strconv"
)
// 项目:前端(网页、小程序、app) 后端代码-接收前端的请求
/*
func http(request) response{
string:= request.getUrlParm
// 数据库
// 计算
// 字符串的转换
}
*/
// string convert = strconv
func main() {
// bool
s1 := "true"
// 转化 - 字符串转bool(解析:parse
// func ParseBool(str string) (bool, error)
b1, err := strconv.ParseBool(s1)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%T,%t\n", b1, b1) // bool,true
// bool转字符串(格式化 format
s2 := strconv.FormatBool(b1)
fmt.Printf("%T,%s\n", s2, s2) // string,true
// 整数->字符串 Format 字符串->整数
s3 := "100000"
// 整数: 数字、进制、大小
// 参数:1、str 2、 进制(10) 3、大小
i1, _ := strconv.ParseInt(s3, 10, 64)
fmt.Printf("%T,%d\n", i1, i1) // int64,100
s4 := strconv.FormatInt(i1, 10)
fmt.Printf("%T,%s\n", s4, s4) // string,100000
// 10进制转换字符串,简便方法 atoi itoa
atoi, _ := strconv.Atoi("-20")
fmt.Printf("%T,%d\n", atoi, atoi) // int,-20
itoa := strconv.Itoa(30)
fmt.Printf("%T,%s\n", itoa, itoa) // string,30
}
```
# time 时间
一切的代码,都是为了解决现实生活中可能出现的业务,时间是很重要的。
time 包
- 获取当前时间
- 格式化时间
```go
package main
import (
"fmt"
"time"
)
// time
func main() {
}
// 获取当前时间 now
func time1() {
// 返回值为Time结构体 : 常量:日月年时分秒 周日-周六 方法:获取常量,计算。
now := time.Now()
year := now.Year()
month := now.Month()
day := now.Day()
hour := now.Hour()
minute := now.Minute()
second := now.Second()
// 2023-2-23 20:40:31
// Printf : 整数补位--02如果不足两位,左侧用0补齐输出
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
}
func time2() {
//time1()
// 打印时间
now := time.Now()
// 时间格式化 2023-02-23 20:43:49
// 格式化模板: yyyy-MM-dd HH:mm:ss
// Go语言诞生的时间作为格式化模板:2006年1月2号下午3点4分
// Go语言格式化时间的代码:2006-01-02 15:04:05 (记忆方式:2006 12 345
// 固定的:"2006-01-02 15:04:05"
fmt.Println(now.Format("2006-01-02 15:04:05")) // 24小时制
fmt.Println(now.Format("2006-01-02 03:04:05 PM")) // 12小时制
fmt.Println(now.Format("2006/01/02 15:04")) // 2023/02/23 20:52
fmt.Println(now.Format("15:04 2006/01/02")) // 20:52 2023/02/23
fmt.Println(now.Format("2006/01/02")) // 2023/02/23
}
// 将字符串格式化为Time对象 (获取到网页传递的时间字符串,需要转化为Time才能在代码中使用)
func time3() {
// 其他地方的时区格式:https://www.zeitverschiebung.net/cn/all-time-zones.html
// 获取时间的时区 // "Asia/Shanghai" 必须要大写 手动构建 ,如果不对,会报未知的时间错误
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
fmt.Println(err)
return
}
// 将字符串解析为时间 Time
timeStr := "2023-02-23 20:53:08"
// layout 格式 时间字符串 时区位置 , 需要和前端传递的格式进行匹配
// func ParseInLocation(layout, value string, loc *Location)
timeObj, _ := time.ParseInLocation("2006-01-02 15:04:05", timeStr, loc)
fmt.Println(timeObj)
}
// 时间戳:更多时候和随机数结合
func time4() {
// 格林威治时间自1970年1月1日(00:00:00 GMT)至当前时间的总秒数
// 时间戳 Unix 1970.1.1 00:00:00 - 当下的一个毫秒数,Unix 时间戳,不会重复的。
now := time.Now()
timestamp1 := now.Unix() // 时间戳
timestamp2 := now.UnixNano() // 纳秒的时间数
fmt.Println(timestamp1)
fmt.Println(timestamp2)
//
// 通过 Unix 转换time对象
timeObj := time.Unix(timestamp1, 0) // 返回time对象
year := timeObj.Year()
month := timeObj.Month()
day := timeObj.Day()
hour := timeObj.Hour()
minute := timeObj.Minute()
second := timeObj.Second()
// 2023-2-23 20:40:31
// Printf : 整数补位--02如果不足两位,左侧用0补齐输出
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
}
```
# 随机数
```go
package main
import (
"fmt"
"math/rand"
"time"
)
// random随机数- math/rand
func main() {
// 获取一个随机数
num1 := rand.Int()
fmt.Println("num1:", num1) // 5577006791947779410
// 随机需要一个随机数的种子,如果种子一样,那么结果一致
// n范围(0-n
num2 := rand.Intn(100)
fmt.Println("num2:", num2) // 7
// 需要一个随时都在发生变化的量 时间戳
timestamp := time.Now().Unix()
// 设置随机数种子, 使用时间戳
// 种子只需要设置一次即可。
rand.Seed(timestamp) // 每次执行都不同
for i := 0; i < 5; i++ {
// Intn [0,n)
// 20-29
num1 := rand.Intn(200) // 抽奖程序
// 必中的逻辑
num2 := rand.Intn(5) // 抽奖程序
if i == num2 {
fmt.Println(10)
continue
}
fmt.Println(num1)
}
}
```
# 定时器-时间操作
> 时间间隔常量 Duration
time.Duration是time包定义的一个类型,它代表两个时间点之间经过的时间
以纳秒为单位,可表示的最长时间段大约290年。
time包中定义的时间间隔类型的常量如下:
```go
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
```
time.Duration表示1纳秒,time.Second表示1秒。
> 定时器
```go
// 定时器: 每隔xxx s执行一次, 其余的关于定时器,放到chan讲
ticker := time.Tick(time.Second) // 每一秒都会触发。
for i := range ticker {
fmt.Println(i)
}
```
> 时间判断
```go
package main
import (
"fmt"
"time"
)
func main() {
// 加 减 比较(在xxx之前 在xxx之后 相等)
now := time.Now()
later := now.Add(time.Hour)
fmt.Println(later)
// 两个时间的差值
subTime := later.Sub(now)
fmt.Println(subTime) // 1h0m0s
// 比较时间, init() 校验时间 当地时间和网络时间是否一致
fmt.Println(now.Equal(later)) // fasle
fmt.Println(now.Before(later)) // true
fmt.Println(now.After(later)) // fasle
}
// 定时器 - 本质是一个通道chan
func d1() {
// 定时器: 每隔xxx s执行一次, 其余的关于定时器,放到chan讲
ticker := time.Tick(time.Second) // 每一秒都会触发。
for i := range ticker {
fmt.Println(i)
}
}
```
小案例:
```go
package main
import (
"syscall"
"time"
"unsafe"
)
func main() {
ticker := time.Tick(time.Second) // 每一秒都会触发。
for i := range ticker {
msgBox(i.Format("2006-01-02 15:04:05"))
}
}
func msgBox(timeStr string) {
user32 := syscall.NewLazyDLL("user32.dll")
messageBox := user32.NewProc("MessageBoxW")
hwnd := 0 // 0表示将弹窗放在桌面的中心位置
title := "Hello"
text := timeStr
flags := 0x00000000 | 0x00000040 // 0x00000000表示弹出消息框并且默认按钮为OK,0x00000040表示消息框的图标为信息图标
messageBox.Call(uintptr(hwnd), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text))), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))), uintptr(flags))
}
```
@@ -0,0 +1,699 @@
# 计算机 I/O 与文件系统基础
## 数据的本质
计算机中的所有数据本质上都是二进制流,即由 0 和 1 组成的序列。不同类型的文件(如图片、视频、文本等)只是这些二进制数据按照特定格式编码和解码的结果。例如:
- 图片文件(JPG):二进制数据按照 JPEG 格式编码,显示器将其解析为可视化的图像
- 视频文件(MP4):二进制数据按照 MP4 编码规范存储,包含视频帧、音频等信息
- 文本文件:二进制数据通过字符编码(如 UTF-8)转换为可读的文字
## I/O 的含义
I/O(输入/输出)是计算机与外部世界交换数据的基础:
- Input(输入):从外部获取数据到计算机系统
- Output(输出):将数据从计算机系统传输到外部
## 文件系统中的文件类型
主要分为两大类:
1. 文本文件:存储人类可读的字符序列
2. 二进制文件:存储原始的二进制数据(如图片、视频、可执行文件等)
## 文件操作的基本功能
现代编程语言通常通过封装底层操作系统的文件 API 来提供文件操作能力。核心功能包括:
1. 获取文件信息(FileInfo
- 文件大小
- 创建时间
- 修改时间
- 访问权限等
2. 文件读取(Read
- 读取文本内容
- 读取二进制数据
- 支持随机访问和顺序读取
3. 文件写入(Write
- 写入文本内容
- 写入二进制数据
- 支持追加和覆盖模式
这种组织方式更系统地展示了这些概念之间的关系,并提供了更详细的解释。你觉得这样的组织方式如何?是否还需要在某些方面做进一步的调整?
```go
package main
import (
"fmt"
"os"
)
// file
// fileInfo
/*
type FileInfo interface {
Name() string // base name of the file
Size() int64 // length in bytes for regular files; system-dependent for others
Mode() FileMode // file mode bits 权限
ModTime() time.Time // modification time
IsDir() bool // abbreviation for Mode().IsDir()
// 获取更加详细的文件信息, *syscall.Stat_t 反射来获取
Sys() any // underlying data source (can return nil)
*/
func main() {
// 获取某个文件的状态
fileinfo, err := os.Stat("D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\test")
if err != nil {
return
}
fmt.Println(fileinfo.Name()) // demo01.go
fmt.Println(fileinfo.IsDir()) // false
fmt.Println(fileinfo.ModTime()) // 2023-02-23 20:25:43.1772351 +0800 CST
fmt.Println(fileinfo.Size()) // 1186 字节数
fmt.Println(fileinfo.Mode()) // -rw-rw-rw-
}
```
## 创建文件、目录
通过代码创建文件
路径:
- 相对路径
- 相对当前目录的路径
- ./ 当前目录
- ../ 上一级目录
- 绝对路径
- 从盘符开始的路径
> 创建目录
mkdir / 权限
mkdirAll 创建层级目录
remove 删除目录
removeAll 强制删除目录
```go
package main
import (
"fmt"
"os"
)
// 创建目录
// 项目开源框架,一运行,就会自动生成脚手架目录
func main() {
// 打开一个文件夹(1、存在我就打开 2、不存在,创建这个文件夹)
// func Mkdir(name string, perm FileMode) error
// ModePerm : 0777
err := os.Mkdir("D:\\MyGo\\src\\github.com\\Docker7530\\testproject\\file1", os.ModePerm)
if err != nil {
// 存在就无法创建了 Cannot create a file when that file already exists.
fmt.Println(err)
}
fmt.Println("文件夹创建完毕")
// 创建层级文件夹
err2 := os.MkdirAll("D:\\MyGo\\src\\github.com\\Docker7530\\testproject\\file2\\aa\\bb\\cc\\dd", os.ModePerm)
if err2 != nil {
fmt.Println(err2)
}
fmt.Println("层级文件夹创建完毕")
// 删除 remove
// func Remove(name string) error
// 通过remove方法只能删除单个空文件夹:
// remove D:\Environment\GoWorks\src\xuego\lesson11\file2: The directory is not empty.
err3 := os.Remove("D:\\MyGo\\src\\github.com\\Docker7530\\testproject\\file2")
if err3 != nil {
fmt.Println(err3)
//return
}
fmt.Println("file delete success!!")
// 如果存在多层文件,removeAll,相对来说比较危险,删除这个目录下的所有东西, 强制删除
err4 := os.RemoveAll("D:\\MyGo\\src\\github.com\\Docker7530\\testproject\\file2")
if err4 != nil {
fmt.Println(err4)
return
}
fmt.Println("err4 delete success!!")
}
```
> 创建文件
os.create(),若存在就是的打开-就是返回的这个file对象. 如果不存在,创建在打开
```go
package main
import (
"fmt"
"os"
)
func main() {
// 创建文件 Create
// func Create(name string) (*File, error) {
// 返回的file对象就是我们的文件
file1, err := os.Create("a.go") // 相对路径
if err != nil {
fmt.Println(err)
}
fmt.Println(file1)
// 删除
os.Remove("D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\a.go")
}
```
## IO 读
1、与文件建立连接
```go
package main
import (
"fmt"
"os"
)
// IO读
func main() {
// 找到这个文件的对象 create 创建、 打开Open
// func Open(name string) (*File, error)
file1, err := os.Open("D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\狂神的Go世界.txt")
if err != nil {
fmt.Println(err)
}
fmt.Println(file1)
// file 类-- 指定的对象
// 打开文件的时候,选定权限: 可读可写的方式打开
// OpenFile(文件名,打开方式:可读、可写...,FileMode , 权限)
file2, err2 := os.OpenFile("D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\狂神的Go世界.txt",
os.O_RDONLY|os.O_WRONLY, os.ModePerm)
if err2 != nil {
fmt.Println(err2)
}
fmt.Println(file2)
// 可以操作这个对象了
}
```
2、读取 file.Read([]byte) ,将file中的数据读取到 []byte 中, n,err n读取到的行数,err 错误,EOF错误,就代表文件读取完毕了
一直调用read,就代表光标往后移动….
```go
package main
import (
"fmt"
"os"
)
// 读取文件数据
func main() {
// 我们习惯于在建立连接时候通过defer来关闭连接,保证程序不会出任何问题,或者忘记关闭
// 建立连接
file, _ := os.Open("狂神的Go世界.txt")
// 关闭连接
defer file.Close()
// 读代码 ,Go 的错误机制,让我们专心可以写业务代码。
// 1、创建一个容器 (二进制文本文件--0100101010 => 读取流到一个容器 => 读取容器的数据)
bs := make([]byte, 2, 1024) // 缓冲区,可以接受我们读取的数据
// 2、读取到缓冲区中。 // 汉字一个汉字占 3个位置
n, err := file.Read(bs)
fmt.Println(n)
fmt.Println(err)
fmt.Println(string(bs)) // 读取到的字符串 ab
// 光标不停的向下去指向,读取出来的内容就存到我们的容器中。
file.Read(bs)
fmt.Println(string(bs)) // 读取到的字符串 cd
file.Read(bs)
fmt.Println(string(bs)) // 读取到的字符串 e
n, err = file.Read(bs)
fmt.Println(n)
fmt.Println(err) // EOF ,读取到了文件末尾。就会返回EOF。
fmt.Println(string(bs)) // 读取到的字符串
n, err = file.Read(bs)
fmt.Println(n)
fmt.Println(err)
fmt.Println(string(bs)) // 读取到的字符串
}
```
## IO 写(权限)
建立连接 (设置权限:可读可写,扩充这个文件的append os.OpenFile
关闭连接
写入 file.write
- file.Write
- file.WriteString
```go
package main
import (
"fmt"
"os"
)
func main() {
fileName := "狂神的Go世界.txt"
// 权限:如果我们要向一个文件中追加内容, O_APPEND, 如果没有,就是从头开始写
file, _ := os.OpenFile(fileName, os.O_WRONLY|os.O_RDONLY|os.O_APPEND, os.ModePerm)
defer file.Close()
// 操作
bs := []byte{65, 66, 67, 68, 69} // A B C D E
n, err := file.Write(bs)
if err != nil {
fmt.Println(err)
}
fmt.Println(n)
// string类型的写入
n, err = file.WriteString("hhahahahah哈哈哈哈哈哈哈")
if err != nil {
fmt.Println(err)
}
fmt.Println(n)
}
```
## 文件复制
```go
package utils
import (
"fmt"
"io"
"os"
)
// Copy 方法需要参数为:source 源文件 ,destination 目标文件
func Copy(source, destination string, bufferSize int) {
// 读取文件
sourceFile, err := os.Open(source)
if err != nil {
fmt.Println("Open错误:", err)
}
// 输出文件 O_WRONLY , O_CREATE 如果不不存在,则会创建
destinationFile, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE, os.ModePerm)
if err != nil {
fmt.Println("OpenFile错误:", err)
}
// 关闭
defer sourceFile.Close()
defer destinationFile.Close()
// 专注业务代码,拷贝
buf := make([]byte, bufferSize)
// 读取
for {
n, err := sourceFile.Read(buf)
if n == 0 || err == io.EOF {
fmt.Println("读取完毕源文件,复制完毕")
break
} else if err != nil {
fmt.Println("读取错误:", err)
return // 错误之后,必须要return终止函数执行。
}
// 将缓冲区的东西写出到目标文件
_, err = destinationFile.Write(buf[:n])
if err != nil {
fmt.Println("写出错误:", err)
}
}
}
```
调用
```go
package main
import "xuego/lesson11/utils"
func main() {
source := "C:\\Users\\遇见狂神说\\Desktop\\xq.png"
dest := "D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\xq.png"
utils.Copy(source, dest, 1024)
}
```
> 系统给我们提供了copy方法
```go
// 调用系统的方法
func Copy2(source, destination string) {
// 读取文件
sourceFile, err := os.Open(source)
if err != nil {
fmt.Println("Open错误:", err)
}
// 输出文件 O_WRONLY , O_CREATE 如果不不存在,则会创建
destinationFile, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE, os.ModePerm)
if err != nil {
fmt.Println("OpenFile错误:", err)
}
// 关闭
defer sourceFile.Close()
defer destinationFile.Close()
// 具体的实现
written, err := io.Copy(destinationFile, sourceFile)
fmt.Println("文件的字节大小:", written)
}
```
## Seeker接口
设置光标的位置,读写文件
```go
package main
import (
"fmt"
"io"
"os"
)
func main() {
// 1. 创建一个测试文件
file, err := os.OpenFile("狂神的Go世界1.txt", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
fmt.Println("打开文件错误:", err)
return
}
defer file.Close()
// 2. 写入初始内容
file.WriteString("Hello World!")
// 3. 演示不同的 Seek 操作
demonstrateSeek(file)
}
func demonstrateSeek(file *os.File) {
// 重置光标到文件开始
file.Seek(0, io.SeekStart)
// 从文件开始读取 5 个字节
buf := make([]byte, 5)
n, err := file.Read(buf)
if err != nil {
fmt.Println("读取错误:", err)
return
}
fmt.Printf("1. 读取前5个字符: %s\n", string(buf[:n]))
// 从当前位置向后移动 1 个字节
file.Seek(1, io.SeekCurrent)
buf = make([]byte, 5)
n, _ = file.Read(buf)
fmt.Printf("2. 跳过1个字符后读取: %s\n", string(buf[:n]))
// 从文件末尾向前移动 5 个字节
file.Seek(-5, io.SeekEnd)
buf = make([]byte, 5)
n, _ = file.Read(buf)
fmt.Printf("3. 从末尾往前5个字符读取: %s\n", string(buf[:n]))
// 在文件末尾追加内容
file.Seek(0, io.SeekEnd)
file.WriteString("\nAppended Text")
// 读取整个文件内容
file.Seek(0, io.SeekStart)
content, _ := io.ReadAll(file)
fmt.Printf("\n4. 最终文件内容:\n%s\n", string(content))
}
```
## 断点续传
思考几个问题:
1、如果你要传的文件很大,70G,是否有方法可以缩短耗时?
- 将文件拆分
- 同时多线程进行下载
2、如果在文件传递过程中,程序被迫中断(断电、断网、内存满了..),下次重启之后,文件是否还需要重头再传?
- 希望能够继续上传或者下载
3、传递文件的时候,支持暂停和恢复上传?假设这个两个操作分布在重启前后?
- 支持!
file、read、write、seek
思路:
1、需要记住上一次传递了多少数据、temp.txt => 记录
2、如果被暂停或者中断了,我们就可以读取这个temp.txt的记录,恢复上传
3、删除temp.txt
![image-20230226203841579](../../../attachment/images-paste/image-20230226203841579.png)
**所有人必须要要全部理解的一段代码**
```go
package main
import (
"fmt"
"io"
"os"
"strconv"
)
// 断点续传
func main() {
// 传输源文件地址
srcFile := "C:\\Users\\遇见狂神说\\Desktop\\client\\gp.png"
// 传输的目标位置
destFile := "D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\server\\gp-upload.png"
// 临时记录文件
tempFile := "D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\temp.txt"
// 创建对应的file对象,连接起来
file1, _ := os.Open(srcFile)
file2, _ := os.OpenFile(destFile, os.O_CREATE|os.O_RDWR, os.ModePerm)
file3, _ := os.OpenFile(tempFile, os.O_CREATE|os.O_RDWR, os.ModePerm)
defer file1.Close()
defer file2.Close()
fmt.Println("file1/2/3 文件连接建立完毕")
// 1、读取temp.txt
file3.Seek(0, io.SeekStart)
buf := make([]byte, 1024, 1024)
n, _ := file3.Read(buf)
// 2、转换成string - 数字。
countStr := string(buf[:n])
count, _ := strconv.ParseInt(countStr, 10, 64)
fmt.Println("temp.txt中记录的值为:", count) // 5120
// 3、设置读写的偏移量
file1.Seek(count, io.SeekStart)
file2.Seek(count, io.SeekStart)
fmt.Println("file1/2 光标已经移动到了目标位置")
// 4、开始读写(复制、上传)
bufData := make([]byte, 1024, 1024)
// 5、需要记录读取了多少个字节
total := int(count)
for {
// 读取数据
readNum, err := file1.Read(bufData)
if err == io.EOF { // file1 读取完毕了
fmt.Println("文件传输完毕了")
file3.Close()
os.Remove(tempFile)
break
}
// 向目标文件中写入数据
writeNum, err := file2.Write(bufData[:readNum])
// 将写入数据放到 total中, 在这里total 就是传输的进度
total = total + writeNum
// temp.txt 存放临时记录数据
file3.Seek(0, io.SeekStart) // 将光标重置到开头
file3.WriteString(strconv.Itoa(total))
}
}
```
## 遍历文件夹
**下去一定要自己实现**
```go
package main
import (
"fmt"
"log"
"os"
)
// cd /d 文件夹路径
// tree /F , 查看当前文件夹下的所有文件
// 遍历文件夹
// 1、读取当前文件夹下的所有文件
// 2、如果是文件夹,进入文件夹,继续读取里面的所有文件
// 3、设置一些结构化代码
func main() {
dir := "D:\\Environment\\GoWorks\\src\\xuego"
tree(dir, 0)
}
// 日常调试测试常用fmt输出 、 工作中or项目中更多是log日志输出
func tree(dir string, level int) {
// 编写层级
tabString := "|--"
for i := 0; i < level; i++ {
tabString = "| " + tabString
}
// 获取目录 ReadDir, 返回目录信息[]DirEntry,多个文件信息
fileInfos, err := os.ReadDir(dir)
if err != nil {
log.Println(err)
}
// 遍历出来所有文件之后,获取里面的单个文件
for _, file := range fileInfos {
// 文件夹中文件的全路径展示
filename := dir + "\\" + file.Name()
fmt.Println(tabString + file.Name())
// 如果是文件夹,再次遍历
if file.IsDir() {
tree(filename, level+1)
}
}
}
```
## bufio
Go语言自带的IO操作包。bufio,使用这个包可以大幅提升文件的读写效率。
buf 缓冲区.
io操作效率本身是还可以的,频繁访问本地磁盘文件(效率低)
所以说 bufio ,提供了一个缓冲区,读和写都先在缓冲区中,最后再一次性读取或者写入到文件里,降低访问本地磁盘的次数。
![image-20230226214440096](../../../attachment/images-paste/image-20230226214440096.png)
bufio写入
```go
package main
import (
"bufio"
"fmt"
"log"
"os"
)
// bufio 的应用
func main() {
file, err := os.Open("D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\demo01.go")
if err != nil {
log.Println(err)
}
defer file.Close()
// 读取文件
// 创建一个bufio包下的 reader对象。
//bufioReader := bufio.NewReader(file)
//buf := make([]byte, 1024)
//n, err := bufioReader.Read(buf)
//fmt.Println("读取到了多少个字节:", n)
// 读取键盘的输入
// 键盘的输入,实际上是流 os.Stdin
inputReader := bufio.NewReader(os.Stdin)
// delim 到哪里结束读取
readString, _ := inputReader.ReadString('\n')
fmt.Println("读取键盘输入的信息:", readString)
}
```
bufio写出
```go
package main
import (
"bufio"
"fmt"
"os"
)
// 写入
func main() {
file, _ := os.OpenFile("D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\a.txt",
os.O_RDWR|os.O_CREATE,
os.ModePerm)
defer file.Close()
// bufio
fileWrite := bufio.NewWriter(file)
writeNum, _ := fileWrite.WriteString("kuangshen")
fmt.Println("writeNum:", writeNum)
// 发现并没有写出到文件,是留在了缓冲区,所以我们需要调用 flush 刷新缓冲区
// 手动刷新进文件
fileWrite.Flush()
}
```
@@ -0,0 +1,410 @@
# 1. 反射基础概念
- **定义**: 在程序运行时动态获取和操作变量信息的机制
- **主要用途**:
- 获取变量类型和值
- 访问和修改结构体字段
- 动态调用方法
- 实现框架底层逻辑
# 2. 使用场景
- **适用场景**:
- 开发框架和脚手架
- 实现通用性工具
- 处理未知类型的数据
- **使用建议**:
- 一般开发中谨慎使用
- 反射操作性能开销较大
- 代码复杂度会增加
# 3. 核心概念
## Type 和 Kind 的区别
```go
// Type: 具体类型
var user User = User{name: "Tom", age: 20}
// Kind: 底层类型分类
var num int = 18
```
- **Type(类型)**:
- 具体的数据类型
- 例如: `User`, `Student`, `int64`
- **Kind(种类)**:
- 类型的底层分类
- 例如: `struct`, `int`, `string`
# 4. reflect 包核心功能
## 主要 API
```go
// 获取类型信息
typeInfo := reflect.TypeOf(变量)
// 获取值信息
valueInfo := reflect.ValueOf(变量)
```
## 常用操作
```go
// 类型判断
if typeInfo.Kind() == reflect.Struct {
// 处理结构体
}
// 获取/修改值
if valueInfo.Kind() == reflect.Int {
value := valueInfo.Int()
valueInfo.Set(reflect.ValueOf(20))
}
```
# 5. 实际应用示例
## 结构体反射
```go
type User struct {
Name string
Age int
}
func inspectStruct(v interface{}) {
t := reflect.TypeOf(v)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("字段名: %s, 类型: %s\n",
field.Name, field.Type)
}
}
```
## 方法反射
```go
func (u User) SayHello(name string) {
fmt.Printf("Hello, %s\n", name)
}
// 通过反射调用方法
func callMethod(v interface{}) {
val := reflect.ValueOf(v)
method := val.MethodByName("SayHello")
args := []reflect.Value{reflect.ValueOf("Tom")}
method.Call(args)
}
```
# 6. 性能考虑
- 反射操作比直接操作慢 10-100 倍
- 建议在以下场景使用:
- 框架开发
- 配置解析
- 序列化/反序列化
- 避免在性能敏感代码中使用
# 7. 最佳实践
1. 优先使用类型断言代替反射
2. 缓存反射结果避免重复操作
3. 谨慎使用反射修改值
4. 做好错误处理
5. 编写清晰的文档说明
```go
package main
import (
"fmt"
"reflect"
)
// 反射
/*
Type : reflect.TypeOf(a) , 获取变量的类型
Value reflect.ValueOf(a) 获取变量的值
*/
func main() {
// 正常编程定义变量
var a int = 3
fmt.Println("type", reflect.TypeOf(a))
fmt.Println("value", reflect.ValueOf(a))
// 根据反射的值,来获取对象对应的类型和数值
// 如果我们不知道这个对象的信息,我们可以通过这个对象拿到代码中的一切。
// Value
v := reflect.ValueOf(a) // string int User
// Kind : 获取这个值的种类, 在反射中,所有数据类型判断都是使用种类。
if v.Kind() == reflect.Float64 {
fmt.Println(v.Float())
}
if v.Kind() == reflect.Int {
fmt.Println(v.Int())
}
fmt.Println(v.Kind() == reflect.Float64)
//fmt.Println(v.Type())
}
```
> 静态类型 & 动态类型
在反射过程中,编译的时候就知道变量类型的就是静态类型、如果在运行时候才知道类型的就是动态类型
静态类型:变量在声明时候给他赋予类型的
```go
var name string // string是静态类型的
var age int // int 是静态类型的
```
动态类型:在运行的时候可能发生变化,主要考虑赋值问题
```go
var A interface{} // interface{} 静态类型
A = 10 // interface{} 静态类型 动态类型 int
A = "xuexiangban" // interface{} 静态类型 动态类型 string
```
**为什么要用反射:**
1、我们需要编写一个函数,但是不知道函数传递给我的参数时什么?没约定好,传入的类型太多,这些类型不能统一表示,反射。
2、我们在某些使用,需要根据条件来判断具体使用哪个函数处理问题,根据用户的输入来决定,这时候就需要对函数的参数进行反射,在运行期间来动态处理。
**为什么不建议使用反射:**
1、和反射相关的代码,不方便阅读,开发中,代码可读性(指标)很重要
2、Go语言是静态类型的语言,编译器可以找出开发时候的错误,如果代码中有大量反射代码,随时可能存在安全问题,panic,项目就终止。
3、反射的性能很低,相对于正常的开发,至少慢2-3个数量级。项目关键位置低耗时,一定是不能使用反射的。更多时候使用约定。
# 反射获取变量信息
**实现拿到一个对象,还原它的本身结构信息**
reflect.TypeOf(v) : 获取变量的类型 Type
- Type.kind , 找到种类
- Type.NumField(),找到里面字段的数量
- Type.Filed(i)
- Type.NumMethod(),找到里面方法的数量
- Type.Method(i)
reflect.ValueOf(v) : 获取变量的值 Value
- Value.Field(i).Interface()
```go
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string
Age int
Sex string
}
func (user User) Say(msg string) {
fmt.Println("User 说:", msg)
}
// toString : 打印结构体信息
func (user User) PrintInfo() {
fmt.Printf("姓名:%s,年龄:%d,性别:%s\n", user.Name, user.Age, user.Sex)
}
func main() {
user := User{"kuangshen", 18, "男"}
reflectGetInfo(user)
}
// 通过反射,获取变量的信息
func reflectGetInfo(v interface{}) {
// 1、获取参数的类型Type , 可能是用户自己定义的,但是Kind一定是内部类型struct
getType := reflect.TypeOf(v)
fmt.Println(getType.Name()) // 类型信息 User
fmt.Println(getType.Kind()) // 找到上级的种类Kind struct
// 2、获取值
getValue := reflect.ValueOf(v)
fmt.Println("获取到value", getValue)
// 获取字段,通过Type扒出字段
// Type.NumField() 获取这个类型中有几个字段 3
// field(index) 得到字段的值
for i := 0; i < getType.NumField(); i++ {
field := getType.Field(i) // 类型
value := getValue.Field(i).Interface() // value
// 打印
fmt.Printf("字段名:%s,字段类型:%s,字段值:%v\n", field.Name, field.Type, value)
}
// 获取这个结构体的方法 , 获取方法的数量
for i := 0; i < getType.NumMethod(); i++ {
method := getType.Method(i)
fmt.Printf("方法的名字:%s,方法类型:%s", method.Name, method.Type)
}
}
/*
type User struct{
Name string
Age int
Sex string
}
func (main.User) PrintInfo(){}
func (main.User) Say(string)
*/
```
# 反射修改变量的值
vaule.CanSet
vaule.SetXXX
```go
package main
import (
"fmt"
"reflect"
)
// 反射设置变量的值
func main() {
var num float64 = 3.14
update(num)
fmt.Println(num)
}
func update(v any) {
// 通过反射修改值,需要操作对象的指针,拿到地址,然后拿到指针对象
pointer := reflect.ValueOf(&v)
newValue := pointer.Elem()
fmt.Println("类型:", newValue.Type())
fmt.Println("判断该类型是否可以修改:", newValue.CanSet())
if newValue.Kind() == reflect.Float64 {
// 通过反射对象给变量赋值
newValue.SetFloat(2.21)
}
if newValue.Kind() == reflect.Int {
// 通过反射对象给变量赋值
newValue.SetFloat(2)
}
}
```
修改结构体变量:通过属性名,来实现修改
```go
func main() {
user := User{"kuangshen", 18, "男"}
value := reflect.ValueOf(&user)
if value.Kind() == reflect.Ptr {
// 获取指针对象
newValue := value.Elem()
if newValue.CanSet() {
// 如果是结构体:1、需要找到对象的结构体字段名
newValue.FieldByName("Name").SetString("狂神")
newValue.FieldByName("Age").SetInt(26)
}
}
fmt.Println(user)
//reflectGetInfo(user)
}
```
# 反射调用方法
> value.MethodByName("PrintInfo").Call(nil)
>
> // 参数构建
>
> args := make([]reflect.Value, 1)
> args[0] = reflect.ValueOf("这反射来调用的")
通过反射调用user方法
```go
user := User{"kuangshen", 18, "男"}
// 通过方法名,找到这个方法, 然后调用 Call() 方法来执行
// 反射调用方法
value := reflect.ValueOf(user)
fmt.Printf("kind:%s,type:%s\n", value.Kind(), value.Type())
value.MethodByName("PrintInfo").Call(nil) // 无参方法调用
// 有参方法调用
args := make([]reflect.Value, 1)
args[0] = reflect.ValueOf("这反射来调用的")
value.MethodByName("Say").Call(args) // 无参方法调用
```
# 反射调用函数
```go
package main
import (
"fmt"
"reflect"
)
// 反射调用函数 func
func main() {
// 通过函数名来进行反射
// Kind func
value1 := reflect.ValueOf(fun1)
fmt.Println(value1.Kind(), value1.Type())
value1.Call(nil)
value2 := reflect.ValueOf(fun2)
fmt.Println(value2.Kind(), value2.Type())
args1 := make([]reflect.Value, 2)
args1[0] = reflect.ValueOf(1)
args1[1] = reflect.ValueOf("hahahhaha")
value2.Call(args1)
vuale3 := reflect.ValueOf(fun3)
fmt.Println(vuale3.Kind(), vuale3.Type())
args2 := make([]reflect.Value, 2)
args2[0] = reflect.ValueOf(1)
args2[1] = reflect.ValueOf("hahahhaha")
resultValue := vuale3.Call(args2)
fmt.Println("返回值:", resultValue)
}
func fun1() {
fmt.Println("fun1:无参")
}
func fun2(i int, s string) {
fmt.Println("fun2:有参 i=", i, " s=", s)
}
func fun3(i int, s string) string {
fmt.Println("fun3:有参有返回值 i=", i, " s=", s)
return s
}
```
@@ -0,0 +1,341 @@
# 泛型(1.18
## 泛型的概念理解
Go语言他是在进化的,不断的迭代,优化功能!
1.0 - 1.19 (1.20)/ 和最开始Go语言比起来发生了很多变化。
1.7 Context
1.11 modules
1.13 error嵌套
1.18 泛型(类型参数)
https://segmentfault.com/a/1190000041634906
泛型的出现,很受期待,但是很少用!场景不多。
> 打印一个切片(传递不同的参数string 、int)
```go
package main
import (
"fmt"
)
func main() {
is := []int{1, 2}
//strs := []string{"kuangshen", "xuexiangban"}
printSlice(is)
//printSlice(strs)
}
// 如何实现一个方法可以打印上面不同类型的切片呢? 反射
func printSlice(s interface{}) {
// 断言 x.(T), 如果x实现了T,那么就将 x转换为T类型
for _, v := range s.([]string) {
fmt.Println(v)
}
}
```
这种情况,传递的参数进来,需要自己做N种判断来进行适配。
> 泛型
思考:不限定参数的类型,让调用的人自己去定义类型。
```go
// 参数的类型是不确定的,让用户自己指定。
// 泛型也是使用 [],和数组很像!
//
func printSlice[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}
```
printSlice , [T any] 泛型的约束!由于我们不确定这个函数的参数类型。我们希望用户给我们传递这个值。
```
print[T string](name T)
print[T string|int](name T)
func Min[T int|int8|int16|int32|int64](a,b T){
}
```
泛型的作用:
1、减少重复性的代码,提高安全性
- **针对不同的类型,写了相同逻辑的代码,我们就可以通过泛型来简化代码!**
2、在1.18版本之前 反射 来实现。 泛型并不能完全取代反射!
泛型:多的多个类型(类型可以是不确定的)
```
var 变量名 数据类型 = 值
var 变量名 T = 值 // 泛型的逻辑
```
## 泛型类型
```go
package main
import "fmt"
// s1 是我们自己定义的类型 ,本质 []int
type s1 []int
type s2 []float64
type s3 []float32
// 我们定义的结构都是一样的,只是它的类型不同,就需要重新定义这么多的类型。
// 思考:是否有一种机制,只定义一个类型就可以代表上面的所有类型?
// 泛型:类型 参数化了! 参数:人为传递的
/*
1、T 说白了就是一个占位符,类型的形式参数,T是不确定的,需要在使用的时候进行传递。
2、由于T类型是不确定的,我们需要加一些约束 int|float64|float32 。告诉编译器我这个T,只接受
int、float64、float32 类型
3、我们这里定义的类型是什么?Slice[T]
*/
// 这种类型的定义方式,带了类型形参,和普通定义类型就完全不同的。
// 普通的定义类型,这个类型只能代表本身一个,泛型类型,我们可以实现,参数类型传递。
// 我们可以在使用的时候来定义类型。
// 语法糖:简化开发
type Slice[T int | float64 | float32] []T
func main() {
var a Slice[int] = []int{1, 2, 3}
fmt.Printf("%T\n", a) // Slice[int]
var b Slice[float64] = []float64{1.0, 2.0, 3.0}
fmt.Printf("%T\n", b) // Slice[float64]
// 不能够赋值(string 不在T的约束当中,不能实例化的)
//var c Slice[string] = []string{"kuangshen","xxx"}
// T是占位符,在使用的时候,必须要实例化为具体的类型。
//var d Slice[T] = []int{1,2,3}
}
func test(name interface{}) {
fmt.Println(name)
}
```
泛型的类型使用
```go
package main
import "fmt"
// 泛型可以用在所有有类型的地方
type MyStruct[T int | string] struct {
Id T
Name string
}
type IprintData[T int|float64|string] interface {
Print(data T)
}
// 通道
type MyChan[T int|string|float64] chan T
func main() {
//T 泛型的参数类型的属性可以远不止一个,所有东西都可以泛型化。
// map(int)string
// map[KEY]VALUE 类型形参(参数是不确定) KEY 、VALUE
// KEY int | string VALUE float32 | float64 约束
// 类型的名字 MyMap[KEY,VALUE], 通过这一个类型,来代表多个类型!--> 泛型
type MyMap[KEY int | string | float64, VALUE float32 | float64 | int] map[KEY]VALUE
// map [string]float64
var score MyMap[string, float64] = map[string]float64{
"go": 9.9,
"java": 8.0,
}
fmt.Println(score)
}
```
> 特殊的泛型
```go
package main
func main() {
// 特殊的泛型类型,泛型的参数时多样的,但是实际类型定义就是int
type AAA[T int|string] int
var a AAA[int] = 123
var b AAA[string] = 123
//var c AAA[string] = "hello" // 底层类型是int
}
```
这里虽然使用了泛型。但是底层类型就是int,所以无论传什么都可以的,但是赋值,只能是int、
这个例子没什么意义。
## 泛型函数
单纯的泛型没啥意义。和函数结合使用, 可以使用调用者(调用者的类型可以自定义,就可以实现泛型。)
```go
package main
import (
"fmt"
)
type MySlice[T int | float32 | int64] []T
func main() {
var s MySlice[int] = []int{1, 2, 3, 4}
fmt.Println(s.sum())
var s1 MySlice[float32] = []float32{1.0, 2.0, 3.0, 4.4}
fmt.Println(s1.sum())
}
// 调用者,类型是不确定的,用户传什么,她就实例化什么。 类型参数化了 , 泛型
// 没有泛型之前, 反射: reflect.ValueOf().Kind() , 也需要很多if,本质是逻辑相同的,只是类型不同!
func (s MySlice[T]) sum() T {
var sum T
for _, v := range s {
sum += v
}
return sum
}
```
泛型可以增加代码的灵活性,降低了可读性!
```go
package main
import (
"fmt"
)
func main() {
var a int = 1
var b int = 2
fmt.Println(Add[int](a, b))
var c float32 = 1.1
var d float32 = 2.2
fmt.Println(Add[float32](c, d))
// 每次都去写T的类型是很麻烦的,支持自动推导!
// Go的泛型语法糖:自动推导 (本质,就是编译器帮我们加上去了,在实际运行,这里T还是加上去的)
fmt.Println(Add(a, b)) // T : int
fmt.Println(Add(c, d)) // T : float32
}
// 真正的Add实现,传递不同的参数都是可以适配的! Add[T] T在调用的时候需要实例化
// 这种带了类型形参的函数就叫做泛型函数,极大的提高代码的灵活心,降低阅读性!
func Add[T int | float32 | float64](a T, b T) T {
return a + b
}
```
1、泛型类型 (自定义类型结合泛型使用)
2、泛型作为接收者 (实现函数的灵活变化)
3、泛型函数(参数是泛型)
计算机底层的运行,绕不过的,你只能简化代码开发,不能简化实际运行操作!
## 自定义泛型
由于约束有时候很多,我们可以定义一些自己的泛型约束(本质是一个接口)
```go
package main
// 泛型的约束提取定义
type MyInt interface {
int|float32|int8|int32 // 作用域泛型的,而不是一个接口方法
}
// 自定义泛型
func main() {
var a int = 10
var b int = 20
GetMaxNum(a,b)
}
func GetMaxNum[T MyInt](a,b T) T {
if a>b {
return a
}
return b
}
```
内置泛型类型:
any (就是一个泛型,表示了go所有的内置类型。interface{}
comparable :表示所有可以比较的类型
新符号 ~,和类型一起出现的,表示支持该类型的衍生类型!
```go
package main
import "fmt"
// int8 衍生类型
type int8A int8
type int8B = int8
// ~ 表示可以匹配该类型的衍生类型
type NewInt interface {
~int8
}
// ~
func main() {
var a int8A = 10
var b int8A = 10
fmt.Println(GetMax(a, b))
}
func GetMax[T NewInt](a, b T) T {
if a > b {
return a
}
return b
}
```
总结泛型:
1、类型参数 T
2、类型集合 T,K map slice
3、类型推断 (简化开发)
@@ -0,0 +1,318 @@
# Web 基础架构
## 1. Web 类型
- **静态 Web**: 所有用户看到的内容完全相同
- **动态 Web**: 根据用户身份、权限等展示不同内容
## 2. Web 应用架构
- **B/S (Browser/Server)**: 基于浏览器的 Web 应用
- **C/S (Client/Server)**: 需要安装的客户端程序
- 优势: 跨平台支持 (Windows/Mac/Linux)
## 3. 网络协议
- **HTTP**:
- 标准端口: 80
- 用途: 传输超文本(文本、图片、视频等)
- **HTTPS**:
- 标准端口: 443
- 特点: 加密传输,更安全
## 4. 通信模型
**请求-响应模式**:
- 请求 (Request):
- 来源: 客户端(浏览器)
- 内容: URL 地址(如 https://www.baidu.com)
- 方式: GET、POST、PUT、DELETE 等
- 响应 (Response):
- 来源: 服务端
- 功能: 处理请求并返回数据
- 主要操作: CRUD(增删改查),占比约 80%
## 5. 工作流程
1. 客户端通过 TCP/IP 连接服务器
2. 发送 HTTP(S) 请求:
- 普通请求: www.example.com/login?username=xxx&pwd=xxx
- 文件上传: upload/video?file=xxx
3. 服务器处理请求
4. 返回响应结果
5. 客户端展示结果
6. 断开连接
## 开发调试
- 使用浏览器开发者工具 (F12 或右键检查)
- 分析请求/响应数据
- 优化代码性能和健壮性
# helloworld
go 语言中所有和网络相关的都在 net 包下,http 也在 net 包下。
包含了客户端和服务端的代码实现。
未来我们学习的所有框架,都是基于这些底层代码的。
请求(request- 响应(response
```go
package main
import (
"fmt"
"net/http"
)
// 一个简单的服务端代码实现
// http程序启动之后是不会停止的,一直监听请求
func main() {
// 写一些请求来接收浏览器的信息
// HandleFunc http请求的处理函数
// func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
// localhost:8080/hello url -> 代码处理
http.HandleFunc("/hello", hello)
// 有一个地址给浏览器访问,什么都没有。404
// func ListenAndServe(addr string, handler Handler)
// localhost 本机(127.0.0.1 端口 8080
// nil默认处理器,空的 404
// 开启监听程序的代码是放在main方法的最后一行的。
http.ListenAndServe("localhost:8080", nil)
}
// 请求和响应
func hello(resp http.ResponseWriter, req *http.Request) {
// 查看一些请求信息 (/login:用户名和密码来匹配登录 /user/id 接收用户的id然后查询用户信息 )
fmt.Println(req.URL)
fmt.Println(req.Method)
fmt.Println(req.RemoteAddr)
//...
// 一般会响应一些信息给客户端 (文字、网页) resp.Write
// 响应一段文字[]byte("hello,web")
// 响应一段html代码 []byte("html代码") 网页
resp.Write([]byte("<h1 style=\"color: red;\">hello,web</h1>"))
}
```
代码来写客户端
```go
package main
import (
"fmt"
"io"
"net/http"
)
// 手写客户端访问
func main() {
/*
http.Get()
*/
// 请求方式 请求的url 接收响应结果
resp, _ := http.Get("http://localhost:8080/hello")
// 通过defer关闭连接 resp.Body 响应的主体
defer resp.Body.Close()
fmt.Println(resp.Body)
fmt.Println(resp.Status) // 200 OK
fmt.Println(resp.Header) // 响应头
// 接收具体的响应内容
buf := make([]byte, 1024)
for {
n, err := resp.Body.Read(buf)
if err != nil && err != io.EOF {
fmt.Println("读取出现了错误")
return
} else {
fmt.Println("读取完毕")
res := string(buf[:n])
fmt.Println("服务器响应的数据为:", res)
break
}
}
}
```
# 带参数的请求
客户端编写
- url的参数拼接 ?拼接 & 连接
```go
package main
import (
"fmt"
"io"
"net/http"
"net/url"
)
func main() {
// 复杂请求
urlStr := "http://127.0.0.1:8080/login" // ?
// 参数如何拼接到url上,参数封装为数据url.Values{}
data := url.Values{}
data.Set("username", "admin") // ?
data.Set("password", "123456") // ?
// 将url字符串转化为url对象,并给携带数据
urlNew, _ := url.ParseRequestURI(urlStr)
urlNew.RawQuery = data.Encode()
// http://127.0.0.1:8080/login?password=123456&username=kuangshen
// ? get的传参,多个参数之间使用 & 连接
fmt.Println(urlNew)
// 发请求,参数是一个地址
resp, _ := http.Get(urlNew.String())
defer resp.Body.Close()
// 读取resp信息,返回buf
buf, _ := io.ReadAll(resp.Body)
fmt.Println(string(buf))
}
```
后台处理代码
```go
func login(resp http.ResponseWriter, req *http.Request) {
// 模拟数据库中存在一个数据
mysqlUserData := "admin"
mysqlPwdData := "123456"
fmt.Println("接收到了login请求")
// 拿到请求中的参数
urlData := req.URL.Query()
username := urlData.Get("username")
password := urlData.Get("password")
// 登录逻辑, 将客户端发送的数据和系统数据比对实现登录业务
if username == mysqlUserData {
if password == mysqlPwdData {
resp.Write([]byte("登录成功"))
} else {
resp.Write([]byte("密码错误"))
}
} else {
resp.Write([]byte("登录失败"))
}
}
```
# 表单参数获取
html表单
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<!--表单-->
<!--http://localhost:8080/register?username=kuangshen&password=123456-->
<form action="http://localhost:8080/register" method="post">
<h2>注册</h2>
<p>用户名:<input type="text" name="username"></p>
<p>密码:<input type="password" name="password"></p>
<input type="submit" value="注册">
</form>
</body>
</html>
```
后端处理代码
```go
func register(resp http.ResponseWriter, req *http.Request) {
fmt.Println("接收到了注册请求")
// 处理表单的请求, 前端提交表单-后盾解析表单
req.ParseForm() // 解析表单
// 获取表单参数 post
username := req.PostForm.Get("username")
password := req.PostForm.Get("password")
fmt.Println(username, password)
// 很多的判断
// 短信
// 验证码
resp.Write([]byte("注册成功"))
}
```
# 要给前端响应数据 (了解即可)
- 我们给前端数据
- 页面模板要渲染数据!有很多语法,看看就好。类似于Java中的JSP 、或者PHP里面的代码
- 代码+页面融合,很乱,已经被这个时代抛弃了
- 前后端分离(我们后面都用这种方式)
1、通过代码跳转到页面 template
```go
// 通过请求,进入页面(路由) temp.Execute(resp, data)
// 通过URl进入某个页面
func findAll(resp http.ResponseWriter, req *http.Request) {
// 接收到前端的信息 /findAll, 查询全部用户
userMap := make(map[int]User)
userMap[1] = User{"KUANGSHEN", 1}
userMap[2] = User{"xiaoming", 2}
// 返回给前端页面并渲染上去
temp, _ := template.ParseFiles("./userlist.html")
data := make(map[string](map[int]User))
data["data"] = userMap
temp.Execute(resp, data)
}
```
2、前端的一些简答语法(了解即可,和php、jsp、C#差不多
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>查询用户</title>
</head>
<body>
<!--获取后端的数据 {{.data}}
遍历
{{range $k,$v := .data}}
{{end}}
-->
{{range $k,$v := .data}}
{{$k}}
{{if eq $k 1}}
{{.Name}}
{{$v}}
{{end}}
{{end}}
</body>
</html>
```
+116
View File
@@ -0,0 +1,116 @@
---
title: 修复Go时区设置
created: 2025-08-02
source: Cherry Studio
tags: []
---
`CGO`(通常写作 **cgo**)是 Go 语言提供的一个工具和机制,**允许 Go 程序调用 C 语言的函数、使用 C 的数据类型、链接 C 的库**。它是 Go 与 C 之间互操作的桥梁。
---
### 🧱 为什么需要 CGO
Go 是一门现代系统编程语言,但它不能(也不打算)完全替代 C 在底层系统中的地位。许多操作系统 API、硬件驱动、已有库(如 OpenSSL、FFmpeg、SQLite)都是用 C 写的。
所以 Go 提供了 **cgo**,让开发者可以在 Go 代码中直接调用这些 C 接口。
---
### 🔧 如何使用 CGO?简单例子
```go
package main
/*
#include <stdio.h>
*/
import "C" // 这是关键:必须导入 "C" 这个伪包
func main() {
C.puts(C.CString("Hello from C!"))
}
```
解释:
- `/* … */` 中的 C 代码会被 cgo 编译处理。
- `import "C"` 不是导入真实包,而是 **启用 cgo 模式**,并提供访问 C 的符号(函数、变量等)。
- `C.puts`:调用 C 标准库的 `puts` 函数。
- `C.CString("…")`:将 Go 字符串转为 C 风格的 `char*`(需要手动释放,在复杂场景下要注意内存管理)。
---
### 🧩 CGO 能做什么?
| 功能 | 示例 |
|------|------|
| ✅ 调用 C 函数 | `C.printf(…)`, `C.open()`, `C.sqrt()` |
| ✅ 使用 C 类型 | `C.int`, `C.struct_stat`, `C.size_t` |
| ✅ 定义 C 代码 | 在 Go 文件的注释中写 C 函数或头文件包含 |
| ✅ 链接外部 C 库 | 通过 `#cgo LDFLAGS: -lxxx` 链接 libcurl、libusb 等 |
| ✅ 传递数据 | Go 和 C 之间传递整数、字符串、结构体指针等(需注意内存模型) |
---
### ⚠️ 使用 CGO 的代价
虽然强大,但 cgo 有缺点:
| 缺点 | 说明 |
|------|------|
| 🐢 性能开销 | 跨 Go/C 边界有额外开销(栈切换、参数转换) |
| 🔗 破坏静态编译 | 使用 cgo 后,编译出的程序依赖系统的 C 库(如 glibc),可能无法跨平台或静态链接 |
| 🧨 安全风险 | C 没有内存安全,容易引入缓冲区溢出、空指针等 bug,可能导致整个 Go 程序崩溃 |
| 📦 构建复杂 | 需要 C 编译器(gcc/clang)环境,交叉编译更麻烦 |
> 因此,Go 社区常说:“**cgo should be avoided unless absolutely necessary.**”
---
### 💡 实际应用场景
1. **操作系统底层调用**
- 如你看到的例子:读取本地时区(调用 `localtime_r`
- 获取网络接口信息、设置 socket 选项等
2. **移动端开发(Android/iOS**
- Android:调用 JNI 或 Bionic C 库
- iOS:调用系统框架(通过 C 封装)
3. **集成已有 C/C++ 库**
- 比如图像处理用 OpenCV(通过 cgo 封装)
- 数据库如 SQLite`github.com/mattn/go-sqlite3` 就用了 cgo
4. **高性能计算或硬件交互**
- GPU、加密芯片、嵌入式设备驱动等
---
### ✅ 小结:什么是 CGO
> **CGO(cgo)是 Go 语言的一个功能,允许你在 Go 程序中调用 C 代码,实现与 C 的互操作。**
- ✅ 用途:访问系统底层 API、复用 C 库
- ❌ 缺点:影响性能、可移植性和安全性
- 📌 原则:能不用就不用;必须用时,尽量封装成干净接口
---
### 📚 官方文档参考
- [https://golang.org/cmd/cgo](https://golang.org/cmd/cgo)
- 关键命令:`go tool cgo file.go`(内部使用)
---
✅ 所以你在代码里看到的:
```go
/*
#include <time.h>
*/
import "C"
```
这就是在启用了 **cgo**,接着调用 C 的 `time()``localtime_r()` 函数 —— 这正是利用 C 的运行时能力来修复 Go 自身在某些平台上时区检测不准确的问题。
+1
View File
@@ -0,0 +1 @@
`GOMAXPROCS` 是 Go 语言中的一个运行时参数,用于控制 Go 程序可以**并行执行的最多操作系统线程(OS threads)数量**,即可以同时运行的 **逻辑处理器(logical processors** 的数量。
@@ -0,0 +1,165 @@
---
title: Go命令行参数初始化
created: 2025-08-02
source: Cherry Studio
tags: []
---
**Go 语言(Golang**`init` 函数,它使用了 Go 的标准库 `flag` 来处理命令行参数。
---
### ✅ 一句话总结这个语法是干什么的:
> 这段代码是在 **程序启动前** 自动运行的一段“初始化设置”,用来 **读取用户通过命令行输入的各种选项**(比如配置文件路径、是否显示版本等),并把它们保存到变量里,方便后面程序使用。
---
## 🧩 一步一步通俗解释
### 1. `func init()` 是什么?
```go
func init() {
// ...
}
```
- `init()` 是 Go 语言中一个**特殊函数**,不需要你手动调用。
- 它会在 **main函数执行之前自动运行**
- 通常用来做**初始化工作**,比如:设置参数、加载配置、注册组件等。
- 你可以有多个 `init` 函数(在不同文件里),Go 会按顺序执行它们。
🎯 你可以把它想象成:
> “程序启动前的闹钟”,它先醒,帮你把房间灯打开、水烧上,等 `main()` 起床时一切就绪了。
---
### 2. `flag.StringVar(…)` 是干嘛的?
这是 Go 的 `flag` 包提供的功能,用来 **定义命令行参数**
比如你在终端输入这样的命令:
```bash
clash -d /my/config/path -f config.yaml -v
```
那么这段代码就是告诉程序:
> “用户可能通过 `-d` 指定目录,通过 `-f` 指定配置文件,通过 `-v` 查看版本……我先把这些选项提前注册好。”
我们来看一个典型的例子:
```go
flag.StringVar(&homeDir, "d", os.Getenv("CLASH_HOME_DIR"), "set configuration directory")
```
拆开解释:
| 部分 | 说明 |
|------|------|
| `flag.StringVar` | 表示我要定义一个**字符串类型**的命令行参数 |
| `&homeDir` | 把用户输入的值存到变量 `homeDir` 里(`&` 是取地址) |
| `"d"` | 命令行短选项名:`-d /path/to/dir` |
| `os.Getenv(…)` | 默认值:如果用户没写 `-d`,就去环境变量里找 `CLASH_HOME_DIR` |
| `"set configuration directory"` | 帮助信息:别人运行 `-h` 时看到的提示 |
📌 举个生活类比:
> 就像你开空调前,允许用户设置几个选项:
> - 温度(`-t 26`
> - 模式(`-m cool`
> - 是否静音(`-s true`
>
> `init()` 函数就是在说:“我先准备好这些按钮,等用户按哪个我就知道怎么反应。”
---
### 3. 各个参数都是什么意思?(简化版解释)
| 参数 | 例子 | 作用 |
|------|------|------|
| `-d` | `-d ~/.clash` | 设置配置文件所在的“主目录” |
| `-f` | `-f config.yaml` | 指定具体的配置文件 |
| `-config` | `-config base64….` | 直接传一个加密过的配置字符串(不用文件) |
| `-ext-ui` | `-ext-ui ./web` | 换一个网页界面目录(比如自己做的UI) |
| `-ext-ctl` | `-ext-ctl 127.0.0.1:9090` | 设置API服务监听地址(给前端控制用) |
| `-ext-ctl-unix` | 用于 Linux/macOS 的高级通信方式(Unix套接字) |
| `-ext-ctl-pipe` | Windows 上的进程通信方式(命名管道) |
| `-secret` | `-secret mypassword123` | 给API加密码保护,防止别人乱访问 |
| `-m` | `-m` | 开启“地理数据模式”(可能用于精准分流) |
| `-v` | `-v` | 显示版本号,然后退出(比如你想看 `clash -v` 输出 v1.2.3 |
| `-t` | `-t` | 只测试配置文件有没有写错,不真正启动 |
---
### 4. `flag.Parse()` 是干嘛的?
```go
flag.Parse()
```
> 这是**真正开始解析命令行输入**的语句。
比如你输入了:
```bash
clash -d /mydir -f my.yaml -v
```
那么 `flag.Parse()` 就会去分析:
- `-d` 后面是 `/mydir` → 存到 `homeDir`
- `-f` 后面是 `my.yaml` → 存到 `configFile`
- `-v` 出现了 → `version` 变成 `true`
✅ 它就像是:“收卷子+批改作业”的动作,前面都是“出题”,它是“阅卷”。
---
## 🧠 总结:通俗理解整段代码
你可以把整个 `init()` 函数看作:
> “程序还没正式开始,但我已经把遥控器准备好了。
> 用户想换台(改配置)、调音量(设API)、看说明书(查版本)……
> 我都提前把按钮做个登记,等他们按下去时,我才知道怎么反应。”
---
## 🎯 举个现实例子
假设你运行这个命令:
```bash
clash -d ~/.myclash -f myconfig.yaml -secret hello123 -v
```
会发生什么?
1. `init()` 函数自动运行(不用你调)
2. 它用 `flag` 注册了各种选项(-d, -f, -secret, -v 等)
3. `flag.Parse()` 解析你输入的内容:
- `homeDir = "~/.myclash"`
- `configFile = "myconfig.yaml"`
- `secret = "hello123"`
- `version = true`
4. 然后程序继续执行 `main()`,main 函数就可以读取这些变量来做后续操作:
- 如果 `-v` 是 true → 打印版本后退出
- 否则 → 加载配置,启动代理服务……
---
## 💡 小知识补充
- `os.Getenv("XXX")`:先看有没有环境变量,有就当默认值,没有就是空。
- 比如你设置了 `export CLASH_CONFIG_FILE=my.yaml`,那不加 `-f` 也会自动读这个。
- Base64 配置字符串:有些场景不能传文件,就用 `-config XXXXX` 传一大串编码过的文本,也能还原出配置。
---
✅ 最后一句话总结:
> 这段 Go 代码就是 **为命令行程序准备“开关和旋钮”**,让用户能灵活控制程序行为,而 `init()` 就是“开机前自检+准备面板”的过程。