Skip to content

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 对比

概念GoJavaScript
并发单元goroutineasync task / Promise
通信channel无直接等价(EventEmitter、Queue)
启动go fn()调用 async fn 或 Promise
等待多个WaitGroup / channelPromise.all
超时select + time.After / contextAbortController + setTimeout
调度runtime 多路复用 M:N单线程事件循环

关键差异

  1. goroutine 不是 Promise——没有 then/catch,错误通过返回值或 channel 传递
  2. channel 是同步原语,unbuffered channel 发送和接收必须配对
  3. 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. 学习检查

  1. goroutine 与 OS 线程有何区别?
  2. unbuffered 和 buffered channel 的行为差异?
  3. 为什么 main 退出会导致其他 goroutine 终止?
  4. select 的 default 分支做什么?
  5. 如何安全地关闭 channel 并通知接收方结束?
  6. 什么情况下会发生 goroutine 泄漏?

12. 下一步

已完成下一知识点关系
goroutine, channel, selectWaitGroup, Mutex, Context协调 goroutine 与共享状态
Phase 03 HTTP每个 HTTP 请求在独立 goroutine 中处理

建议学习 sync 和 Context,然后进入 Phase 03 Web 开发。


学习导航

上一篇:Go Package Design · 对应练习 · 下一篇:Sync and Context