今日目标:Python 的多线程受到 GIL 的限制,属于“伪并发”。而 Go 的并发是真并行。今天我们将学习如何启动成千上万个轻量级线程 (Goroutine),并使用 Channel 在它们之间优雅地传递数据,实现 “不要通过共享内存来通信,而要通过通信来共享内存” 的 Go 核心哲学。
学习内容 (30 mins)
在开始写代码前,先搞懂这些核心概念,否则后面的代码你会看得云里雾里。Goroutine (协程) (10 mins)
Goroutine (协程) (10 mins)
为什么 Go 能轻松支持百万级并发?
- OS 线程: JVM 或 Python 调用的通常是操作系统线程,一个就要消耗约 2MB 内存,启动和切换都很慢。
- Goroutine: 这是由 Go 运行时管理的“用户态线程”。启动一个只需约 2KB 内存。
- 使用方法: 在任何函数前输入
go即可(例如go task())。 - 生命周期: 主线程
main像是一个指挥官,如果指挥官下班了(main执行结束),他在外面派出的所有斥候(Goroutines)都会被强制瞬间销毁。此特性需要特别注意。
Channel (通道) (10 mins)
Channel (通道) (10 mins)
并发里的安全传送带
- 痛点: 最怕多个线程同时抢着改同一个变量(竞态条件),导致数据乱套。
- Go 的解决方案: 既然抢数据会打架,那我们就把数据通过 Channel 传过去,谁接到谁处理。
- 语法:
- 创建:
ch := make(chan int) - 发送:
ch <- 1(向通道塞数据) - 接收:
x := <-ch(从通道拿数据)
- 创建:
- 阻塞特性: 无缓冲的 channel,发送方会阻塞直到有人接收,接收方也会阻塞直到有人发送。这种阻塞是天生的同步机制。
WaitGroup (同步) (10 mins)
WaitGroup (同步) (10 mins)
如何优雅地等待?
sync.WaitGroup 是一个计数器,用来等待一组 Goroutine 完成。Add(n): “报告长官,派出了 n 个人!”Done(): “报告长官,我干完活了!”(计数器 -1)Wait(): 阻塞主线程,直到计数器归零。
代码任务 (90 mins)
任务 A: 感受并发的“随机性”
通过这个例子,你会理解为什么并发程序需要协调。代码解释:
go task("Agent-001"):启动一个 Goroutine,异步执行time.Sleep(500 * time.Millisecond):主线程等待,防止提前退出- 并发执行:两个 Goroutine 同时运行,输出顺序不确定
- “不要通过共享内存来通信,而要通过通信来共享内存”:这是 Go 的核心哲学
- 轻量级:Goroutine 比线程轻量得多,可以轻松创建成千上万个
- 自动调度:Go 运行时自动在多个 OS 线程上调度 Goroutine
- 运行程序,观察输出顺序。你会发现 Agent-001 和 Agent-002 的输出是交替的,而且每次运行的交替顺序可能都不一样。
- 尝试注释掉
time.Sleep那一行,再运行。你会发现很可能什么都打印不出来,程序就结束了。 - 多次运行,观察输出顺序的变化,理解并发的非确定性
任务 B: 生产者-消费者模式 (Channel)
模拟一个“日志收集器”:抓取器负责生产日志,存储器负责消费并保存。这是 Channel 最经典的应用场景。代码解释:
chan<- string:只发送通道(Send Only)<-chan string:只接收通道(Receive Only)close(logQueue):关闭通道,通知接收方没有更多数据for msg := range logQueue:优雅地从通道读取,直到通道关闭
- 通信即同步:Channel 既是数据传递的通道,也是同步的机制
- 阻塞特性:无缓冲通道天然提供同步,发送方和接收方必须同时准备好
- 关闭信号:关闭通道是一种信号,表示没有更多数据
- 运行程序:
go run 24_channel.go - 观察生产者和消费者的协作过程
- 尝试注释掉
close(logQueue),观察程序是否会死锁
任务 C: WaitGroup 并发大杀器
模拟同时对多个 IP 进行 Ping 操作。这是以后写并发脚本最常用的模板。代码解释:
*sync.WaitGroup:必须传指针,否则是副本,Done()不会生效defer wg.Done():使用 defer 确保即使函数出错也会调用 Donewg.Add(1):在启动 Goroutine 前增加计数wg.Wait():阻塞直到所有 Goroutine 完成
- 计数器模式:通过计数跟踪 Goroutine 的完成状态
- 必须传指针:WaitGroup 是值类型,必须传指针才能共享状态
- defer 保护:使用 defer 确保 Done 一定会被调用
- 运行程序:
go run 24_waitgroup.go - 观察所有主机检查的并发执行
- 尝试修改
wg.Add(1)的位置,观察行为变化
拓展任务 (30 mins)
带缓冲的 Channel 挑战
任务:将
make(chan string) 改为 make(chan string, 10)。实验:在 LogProducer 里不加 Sleep。观察运行行为。- 现象: 生产者会瞬间把 4 条日志塞进去然后退出,而不是像无缓冲那样必须等消费者拿走一条才能塞下一条。
- 思考: 这种模式适合应对什么样的流量高峰?
Timeout 挑战 (Select 语句)
任务:查阅
select 关键字。挑战:在主线程接收 <-done 时,如果过了一秒还没干完,就打印“Timeout! Program terminated.”。这是防止程序hang死的常见手段。今日产出物
~/projects/learn-go/24_race.go- 并发竞争演示~/projects/learn-go/24_channel.go- 经典解耦流水线~/projects/learn-go/24_waitgroup.go- 批量处理同步模板
参考代码
查看参考代码
在 GitHub 查看完整的示例代码
Go Playground
使用在线编辑器运行 Go 代码
实际应用场景
为什么 Go 开发的 Agent 强?
- 极低抖动: 即使监控成千上万个文件,Goroutine 的上下文切换开销也极小,不会导致 CPU 突然飙升。
- 原生的超时控制: 在处理网络请求时,使用
context结合select可以完美控制每个连接的超时,防止把系统卡死。
并发 vs 并行
- 并发 (Concurrency): 多个任务在一个时间段内运行(可能是单核上的快速切换)。
- 并行 (Parallelism): 多个任务在同一个时刻同时运行(需要多核 CPU 支持)。
- Go 的优势: 它自动帮你在这两者之间调度,你只需要关心
go关键字。
回到目录
查看完整进度
下一天: 工具开发 I
Day 25 | Go 实战工具开发 (一)