惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Blog — PlanetScale
Blog — PlanetScale
SecWiki News
SecWiki News
Google DeepMind News
Google DeepMind News
WordPress大学
WordPress大学
小众软件
小众软件
C
CERT Recently Published Vulnerability Notes
Jina AI
Jina AI
N
Netflix TechBlog - Medium
GbyAI
GbyAI
IT之家
IT之家
Apple Machine Learning Research
Apple Machine Learning Research
AWS News Blog
AWS News Blog
G
GRAHAM CLULEY
L
Lohrmann on Cybersecurity
C
Cybersecurity and Infrastructure Security Agency CISA
I
Intezer
T
Tor Project blog
P
Palo Alto Networks Blog
P
Privacy & Cybersecurity Law Blog
P
Privacy International News Feed
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Proofpoint News Feed
T
Tailwind CSS Blog
C
Check Point Blog
Cloudbric
Cloudbric
Y
Y Combinator Blog
The Last Watchdog
The Last Watchdog
Forbes - Security
Forbes - Security
Last Week in AI
Last Week in AI
S
Security Affairs
博客园 - Franky
F
Fortinet All Blogs
量子位
M
MIT News - Artificial intelligence
C
Cisco Blogs
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
S
Secure Thoughts
V
Visual Studio Blog
AI
AI
美团技术团队
B
Blog RSS Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
博客园 - 三生石上(FineUI控件)
阮一峰的网络日志
阮一峰的网络日志
Engineering at Meta
Engineering at Meta
人人都是产品经理
人人都是产品经理
Microsoft Security Blog
Microsoft Security Blog
T
Threatpost
Cyberwarzone
Cyberwarzone

博客园 - 我才是银古

第16章:常见问题、排错与最佳实践 第15章:扩展生态、MCAD 与外部集成 第12章:实战案例:机械结构与 3D 打印零件 第14章:构建、测试、调试与贡献流程 第13章:OpenSCAD 源码架构与核心执行流程 第11章:预览、渲染、网格精度与性能优化 第09章:列表推导、递归与算法建模 第08章:参数化零件库与复用设计 第10章:导入导出、命令行与自动化 第06章:CSG 布尔建模方法 第07章:二维图形、拉伸、旋转与投影 第05章:基础几何、坐标系与变换 第04章:参数、变量、函数、模块与作用域 OpenSCAD 教程目录 第03章:OpenSCAD 语言基础 第02章:安装、环境配置与开发工作流 第01章:OpenSCAD 项目全景与学习路线 第02章:源码获取、编译与开发环境配置 第01章:OCCT项目全景与学习路线 第18章:二次开发实战与综合案例 第18章:综合实战案例 第17章:数据交换与协同 第16章:源码架构与二次开发 第15章:插件与自定义工作台开发 第14章:Python脚本宏与自动化 第13章:FEM仿真分析 第12章:CAM数控加工 第11章:SurfaceMesh与逆向工程 第10章:Draft二维绘图与BIM建筑 第09章:工程图TechDraw 第07章:参数化表达式与Spreadsheet 第08章:装配设计Assembly 第06章:Part工作台与几何内核 第05章:PartDesign实体特征建模 第04章:草图Sketcher约束建模 第02章:安装版本与工作环境配置 第03章:界面工作台与基础操作 第01章:项目全景与学习路线 第十二章:插件开发、研究功能与最佳实践 第十章:定时任务与自动化(Cron) 第七章:技能、记忆与自学习闭环 第八章:MCP 集成与上下文文件 第六章:工具系统与终端后端 第五章:模型供应商与配置体系 Hermes Agent 教程目录 第十一章:语音、视觉、浏览器与子代理协作 第四章:CLI/TUI 与会话管理 第十二章:学习路线、实战方案与最佳实践 第十一章:源码结构、开发调试与插件开发 第十章:自动化、远程访问、日志与排障 第九章:Control UI、节点、Canvas 与语音能力 第七章:工具、技能、插件与能力扩展 第八章:安全模型、访问控制与沙箱实践 第六章:Agent 工作区、会话与多智能体路由 第五章:多通道消息接入与聊天平台配置 第四章:配置体系、模型接入与认证管理 第三章:Gateway 架构、协议与运行机制 第二章:安装、环境准备与快速上手 第一章:OpenClaw 项目概览与核心定位 oh-my-openagent 教程目录 09-命令模型回退与配置参考 10-实战案例最佳实践与故障排除 05-工作模式-Ultrawork-Prometheus-Atlas 08-Hooks与MCP系统 06-Category与Skill系统 07-核心工具链 04-智能体全景详解 03-安装与环境配置 02-整体架构与多模型编排机制 01-项目简介与核心理念 01-项目概览与学习路线 02-安装部署与工具适配 03-Skill机制与using-superpowers 05-TDD系统化调试与完成前验证 04-需求澄清方案设计与计划编写 07-并行智能体子智能体与Git-Worktree 第六章:代码审查、反馈处理与分支收尾 08-中国特色Skills与本土团队落地 09-MCP构建工作流执行与自定义Skill 第23章:FreeCAD-Python-API Clipper2 C# 源码解读教程 第19章:PolyTree 多边形树结构 第20章:实际应用与最佳实践 第18章:Minkowski 和与差 第17章:RectClip 矩形裁剪优化 第16章:ClipperOffset 偏移类详解 第15章:填充规则详解 第14章:布尔运算执行流程 第13章:ClipperD 浮点裁剪类 第11章:OutRec 与 OutPt 输出结构 第9章:Active 活动边结构 第10章:Vertex 顶点与 LocalMinima 局部极小值 第12章:Clipper64 裁剪类详解 第7章:高精度运算与128位整数 第8章:ClipperBase 基类详解 第5章:枚举类型与常量定义 第6章:InternalClipper 内部工具类 第2章:核心数据结构 - Point64、PointD 第3章:路径与多边形表示 - Path64、PathD、Paths64、PathsD 第4章:矩形边界 - Rect64、RectD
第08章 - 并发编程:Goroutine与Channel
我才是银古 · 2026-06-19 · via 博客园 - 我才是银古

第08章 - 并发编程:Goroutine与Channel

并发是 Go 最引以为傲的特性。本章深入讲解 goroutine 和 channel——Go 并发编程的两大基石,以及基于它们的 CSP 并发模型。

8.1 并发与并行

首先厘清两个概念:

  • 并发(Concurrency):在同一时间段内处理多个任务,任务交替执行(即使在单核 CPU 上也能实现)。
  • 并行(Parallelism):在同一时刻同时执行多个任务,需要多核 CPU。

Rob Pike 有句名言:"并发不是并行(Concurrency is not parallelism)"。并发是一种程序结构的设计方式,使程序能够处理多个独立任务;并行是这种结构在多核硬件上的运行表现。Go 让你用并发的方式编写程序,运行时再将其映射到可用的 CPU 核心上并行执行。

8.2 Goroutine

goroutine 是 Go 运行时管理的轻量级线程。在函数调用前加上 go 关键字,即可在新的 goroutine 中并发执行该函数:

func say(s string) {
    for i := 0; i < 3; i++ {
        fmt.Println(s)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    go say("world") // 在新 goroutine 中执行
    say("hello")    // 在主 goroutine 中执行
}

8.2.1 goroutine 的优势

  • 极轻量:初始栈仅 2KB,按需增长/收缩,可以轻松创建数十万个。相比之下,操作系统线程通常占用 1-8MB 栈空间。
  • 由 Go 运行时调度:Go 使用 M:N 调度模型(详见第 14 章),将大量 goroutine 多路复用到少量操作系统线程上,切换成本远低于线程切换。
  • 创建简单:一个 go 关键字即可。

8.2.2 主 goroutine 退出问题

main 函数本身运行在主 goroutine 中。当主 goroutine 结束时,整个程序立即退出,不会等待其他 goroutine 完成

func main() {
    go fmt.Println("这行可能不会被打印")
    // main 立即结束,程序退出
}

因此需要某种同步机制确保主 goroutine 等待子 goroutine 完成,这正是 channel 和 sync.WaitGroup 的用武之地。

8.3 Channel

channel 是 goroutine 之间通信的管道,遵循 CSP 模型"通过通信共享内存"的理念。channel 是类型化的,只能传递特定类型的数据。

8.3.1 创建与基本操作

ch := make(chan int)    // 创建一个传递 int 的无缓冲 channel

ch <- 42                // 发送:将 42 发送到 channel
value := <-ch           // 接收:从 channel 接收数据
close(ch)               // 关闭 channel

<- 是 channel 操作符,箭头方向表示数据流向。

8.3.2 无缓冲 channel

无缓冲 channel 的发送和接收是同步的:发送操作会阻塞,直到有另一个 goroutine 执行接收;反之亦然。这种特性天然提供了 goroutine 之间的同步:

func main() {
    ch := make(chan string)
    go func() {
        time.Sleep(time.Second)
        ch <- "任务完成" // 发送,阻塞直到被接收
    }()
    msg := <-ch // 接收,阻塞直到有数据,实现了等待
    fmt.Println(msg)
}

8.3.3 有缓冲 channel

有缓冲 channel 拥有一个容量,只要缓冲区未满,发送就不会阻塞;只要缓冲区非空,接收就不会阻塞:

ch := make(chan int, 3) // 容量为 3 的缓冲 channel
ch <- 1                 // 不阻塞
ch <- 2                 // 不阻塞
ch <- 3                 // 不阻塞
ch <- 4                 // 阻塞!缓冲区已满

有缓冲 channel 常用于限流、解耦生产者和消费者的速度差异。

8.3.4 关闭 channel

发送方负责关闭 channel,表示"不会再有数据了"。接收方可以通过第二个返回值判断 channel 是否已关闭:

v, ok := <-ch
// ok 为 false 表示 channel 已关闭且数据已取完

重要规则

  • 只有发送方应该关闭 channel,接收方不应关闭。
  • 向已关闭的 channel 发送数据会 panic。
  • 从已关闭的 channel 接收数据会立即返回零值。
  • 重复关闭 channel 会 panic。

8.3.5 用 range 遍历 channel

range 会持续从 channel 接收数据,直到 channel 被关闭:

func producer(ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch) // 必须关闭,否则 range 会永久阻塞
}

func main() {
    ch := make(chan int)
    go producer(ch)
    for v := range ch { // 自动接收直到 channel 关闭
        fmt.Println(v)
    }
}

8.3.6 单向 channel

可以限定 channel 只能发送或只能接收,增强类型安全和代码可读性:

func send(ch chan<- int) { // 只能发送
    ch <- 1
}

func receive(ch <-chan int) { // 只能接收
    fmt.Println(<-ch)
}

8.4 select 多路复用

select 语句让一个 goroutine 可以同时等待多个 channel 操作,哪个 channel 就绪就执行对应分支:

select {
case msg1 := <-ch1:
    fmt.Println("收到 ch1:", msg1)
case msg2 := <-ch2:
    fmt.Println("收到 ch2:", msg2)
case ch3 <- "data":
    fmt.Println("发送到 ch3")
default:
    fmt.Println("没有就绪的 channel") // 非阻塞
}
  • 若多个 case 同时就绪,select随机选择一个执行。
  • default 分支使 select 变为非阻塞;没有 default 时,select 会阻塞直到某个 case 就绪。

8.4.1 超时控制

select 配合 time.After 是实现超时的经典模式:

select {
case result := <-ch:
    fmt.Println("收到结果:", result)
case <-time.After(2 * time.Second):
    fmt.Println("超时!")
}

8.4.2 优雅退出

通过一个专门的 done channel 通知 goroutine 退出:

func worker(done <-chan struct{}) {
    for {
        select {
        case <-done:
            fmt.Println("收到退出信号")
            return
        default:
            // 执行工作
        }
    }
}

8.5 经典并发模式

8.5.1 Worker Pool(工作池)

固定数量的 worker goroutine 从任务 channel 中取任务处理,控制并发度:

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        results <- j * 2 // 处理任务
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // 启动 3 个 worker
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // 发送 9 个任务
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)

    // 收集结果
    for a := 1; a <= 9; a++ {
        fmt.Println(<-results)
    }
}

8.5.2 Fan-out / Fan-in

  • Fan-out:多个 goroutine 从同一个 channel 读取数据,分散处理。
  • Fan-in:将多个 channel 的数据汇聚到一个 channel。

8.5.3 Pipeline(流水线)

将一系列处理阶段串联,每个阶段是一个 goroutine,通过 channel 连接,形成数据处理流水线:

func gen(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

// 使用: gen -> square
for n := range square(gen(2, 3, 4)) {
    fmt.Println(n) // 4 9 16
}

8.6 常见并发陷阱

  • 死锁(Deadlock):所有 goroutine 都在等待,无人推进。如向无缓冲 channel 发送但无人接收。Go 运行时会检测全局死锁并 panic。
  • goroutine 泄漏:goroutine 永久阻塞无法退出,持续占用资源。务必确保每个 goroutine 都有明确的退出路径。
  • 闭包捕获循环变量:在 Go 1.22 之前,循环变量在迭代间共享,闭包中需显式传参。Go 1.22 起每次迭代创建新变量,此问题已修复。

8.7 本章小结

本章深入讲解了 Go 并发编程的核心:goroutine 是极轻量的并发执行单元,channel 是它们之间类型安全的通信管道。我们学习了无缓冲/有缓冲 channel 的语义、channel 的关闭与遍历、select 多路复用与超时控制,以及 Worker Pool、Pipeline 等经典并发模式。掌握"通过通信共享内存"的思想,是写好 Go 并发程序的关键。

下一章我们将学习并发进阶内容:sync 包提供的传统同步原语及更多并发控制工具。