外观
Goroutine, Channel, Select
Phase 02 — Go Core 涵盖:
goroutine·channel·select
1. 学习目标
完成本知识点后,你应该能够:
- 使用
go关键字启动 goroutine - 理解 goroutine 与 OS 线程的区别
- 使用 channel 在 goroutine 间传递数据和同步
- 区分 buffered 与 unbuffered channel
- 使用
select监听多个 channel - 用 channel 实现数字孪生场景下的并发数据采集
2. 为什么需要
数字孪生后端需要同时处理多台设备:拉取 PLC 数据、写入缓存、推送给 WebSocket 客户端。如果用单线程顺序处理,一台设备阻塞会拖累全部。
Go 的并发模型是 CSP(Communicating Sequential Processes):goroutine 通过 channel 通信,而非共享内存加锁(虽然锁仍有用,见下一知识点)。这比 JS 的 async/await 更底层,但提供了精确的控制力——适合高并发 I/O 服务。
3. 核心概念
3.1 goroutine
轻量级协程,由 Go runtime 调度。启动方式:
go
go doWork()主 goroutine 退出时,所有子 goroutine 会被强制终止——必须等待它们完成(WaitGroup 或 channel)。
3.2 channel
类型化的管道:chan T
| 类型 | 说明 |
|---|---|
chan int | 双向 channel |
chan<- int | 只发送 |
<-chan int | 只接收 |
3.3 unbuffered vs buffered
go
ch := make(chan int) // unbuffered:发送阻塞直到有人接收
ch := make(chan int, 10) // buffered:缓冲 10 个,满则阻塞3.4 select
类似 switch,但监听 channel 操作:
go
select {
case msg := <-ch1:
handle(msg)
case ch2 <- value:
send()
case <-time.After(time.Second):
timeout()
default:
// 非阻塞
}3.5 关闭 channel
go
close(ch)
v, ok := <-ch // ok=false 表示 channel 已关闭且无数据4. 基础语法
完整示例见 workspace/phase-02/goroutine-channel-select/main.go。
go
package main
import (
"fmt"
"sync"
"time"
)
func fetchTelemetry(id string, out chan<- string, wg *sync.WaitGroup) {
defer wg.Done()
time.Sleep(100 * time.Millisecond) // 模拟 I/O
out <- fmt.Sprintf("%s: ok", id)
}
func main() {
ids := []string{"AGV-001", "AGV-002", "AGV-003"}
out := make(chan string, len(ids))
var wg sync.WaitGroup
for _, id := range ids {
wg.Add(1)
go fetchTelemetry(id, out, &wg)
}
// 等待全部完成后关闭 channel
go func() {
wg.Wait()
close(out)
}()
// 读取所有结果
for msg := range out {
fmt.Println(msg)
}
// select 示例:超时
ch := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
ch <- "slow device"
}()
select {
case v := <-ch:
fmt.Println("got:", v)
case <-time.After(500 * time.Millisecond):
fmt.Println("timeout waiting for device")
}
}5. 代码解析
go
go fetchTelemetry(id, out, &wg)启动 goroutine,不阻塞当前执行。注意循环中传参要用 id 的值拷贝(Go 1.22+ for 循环变量语义已修复,旧版本需注意闭包陷阱)。
go
out := make(chan string, len(ids))buffered channel 允许发送方在接收方就绪前暂存结果,减少阻塞。
go
for msg := range out { ... }range channel 会在 channel 关闭且 drained 后退出。
go
select { case <-time.After(...): ... }超时模式:避免 goroutine 永久阻塞。Phase 02 下一节会学 context.Context 做更规范的超时取消。
6. JavaScript / TypeScript 对比
| 概念 | Go | JavaScript |
|---|---|---|
| 并发单元 | goroutine | async task / Promise |
| 通信 | channel | 无直接等价(EventEmitter、Queue) |
| 启动 | go fn() | 调用 async fn 或 Promise |
| 等待多个 | WaitGroup / channel | Promise.all |
| 超时 | select + time.After / context | AbortController + setTimeout |
| 调度 | runtime 多路复用 M:N | 单线程事件循环 |
关键差异:
- goroutine 不是 Promise——没有 then/catch,错误通过返回值或 channel 传递
- channel 是同步原语,unbuffered channel 发送和接收必须配对
- JS 并发 I/O 靠事件循环;Go 靠 goroutine + 阻塞式 I/O(runtime 自动调度)
7. 常见错误
错误 1:main 退出导致 goroutine 被杀
go
go doWork()
// main 结束,doWork 可能没执行完
// ✅ 用 WaitGroup 或 channel 等待错误 2:向 closed channel 发送
go
close(ch)
ch <- 1 // ❌ panic错误 3:goroutine 泄漏
go
ch := make(chan int)
go func() { ch <- 1 }()
// 没有接收者,永远阻塞 → 泄漏错误 4:误用 default 忙等
go
for {
select {
default:
// ❌ CPU 100% 空转
}
}错误 5:共享变量不加保护
多个 goroutine 写同一变量需 Mutex 或只用 channel 通信。
8. 实际应用
数字孪生 — 并发拉取设备状态
go
func FetchAllDevices(ids []string) map[string]State {
out := make(chan Result, len(ids))
var wg sync.WaitGroup
for _, id := range ids {
wg.Add(1)
go func(id string) {
defer wg.Done()
state, err := fetchOne(id)
out <- Result{ID: id, State: state, Err: err}
}(id)
}
go func() { wg.Wait(); close(out) }()
// collect results...
}比顺序 fetch 快 N 倍(I/O bound 场景)。
数据管道
[采集 goroutine] → ch → [解析 goroutine] → ch → [推送 goroutine]每个阶段独立 goroutine,channel 解耦。
9. 深入理解
9.1 goroutine 调度
Go runtime 使用 M:N 调度:M 个 goroutine 映射到 N 个 OS 线程。阻塞 I/O 时 goroutine 让出,不阻塞线程。
9.2 channel 方向
函数参数用 chan<- T(只发)或 <-chan T(只收)表达意图,编译器防止误用。
9.3 fan-in / fan-out
- fan-out:一个任务分发到多个 worker goroutine
- fan-in:多个 channel 合并到一个(select 或额外 goroutine)
9.4 与 Context 配合
长时间运行的 goroutine 应监听 ctx.Done(),收到取消信号后退出,避免泄漏。
10. 练习
详细题目见
exercises/phase-02-core/05-goroutine-channel-select.md。
Level 1 — 基础
练习 1.1:启动 goroutine 打印 1–5,main 用 time.Sleep 等待。
练习 1.2:创建 unbuffered channel,一个 goroutine 发送,main 接收。
练习 1.3:使用 buffered channel,验证发送不立即阻塞。
Level 2 — 应用
练习 2.1:并发 fetch 3 个设备 ID,结果汇总到 slice。
练习 2.2:用 select 实现 1 秒超时。
练习 2.3:用 range 读取直到 channel close。
Level 3 — 综合
练习 3.1:实现 worker pool:5 个 worker 处理 20 个任务。
练习 3.2:fan-in:合并两个 channel 的数据到一个。
Level 4 — 项目实践
练习 4.1:在 workspace/phase-02/goroutine-channel-select/ 构建并发 telemetry 采集器:多设备 goroutine + 结果 channel + 超时 select。
11. 学习检查
- goroutine 与 OS 线程有何区别?
- unbuffered 和 buffered channel 的行为差异?
- 为什么 main 退出会导致其他 goroutine 终止?
select的 default 分支做什么?- 如何安全地关闭 channel 并通知接收方结束?
- 什么情况下会发生 goroutine 泄漏?
12. 下一步
| 已完成 | 下一知识点 | 关系 |
|---|---|---|
| goroutine, channel, select | WaitGroup, Mutex, Context | 协调 goroutine 与共享状态 |
| Phase 03 HTTP | 每个 HTTP 请求在独立 goroutine 中处理 |
建议学习 sync 和 Context,然后进入 Phase 03 Web 开发。