第一部分:并发基础
第 1 章:Go 并发编程概述
1.1 前言
Go 语言的设计背景,是 Google 工程师对当时主流服务端语言的一次深刻反思。
彼时,Java 和 C++ 是编写服务器程序最常用的语言。这些语言的确可以支撑高效的开发,但它们存在一些痛点:语法规则繁多,样板代码重复,并发编程门槛高。部分开发者因此转向更动态、更流畅的语言,如 Python,但随之而来的代价是运行效率的下降和静态类型安全检查的缺失。
Go 的设计者们认为,这种两难困境是可以突破的------他们希望发明一种语言,能同时具备高开发效率、静态类型安全和简洁流畅的代码风格。于是,Go 语言诞生了。
1.2 Go 为什么天生适合并发
Google 内部大量服务运行在多核心 CPU 机器上,面对高并发流量,并发编程能力至关重要。C++ 和 Java 在语言层面对并发的原生支持并不理想,使用它们编写高并发程序往往需要开发者付出大量额外的心智成本。
Go 的设计者们从一开始就将并发作为语言的一等公民,在语言层面内置了并发支持。这不是简单地在标准库里加几个工具类,而是在语言的核心抽象层面重新思考了并发的表达方式。
1.3 传统并发模型:共享内存
传统的线程模型(Java、C++、Python 均采用此模型)的核心思想是:多个线程通过共享内存进行通信。
当多个线程需要交换信息或协同工作时,它们通过读写堆上的共享变量来实现,而这些共享变量必须用锁来保护。在实践中,这种模式会带来以下挑战:
-
锁的粒度难以把握------锁太粗会降低并发度,锁太细又容易引入死锁;
-
死锁和条件竞争是常见的、且极难复现和调试的 Bug;
-
随着系统复杂度提升,锁的管理负担会呈指数级增长。
为了在这种模型下更安全地编程,Java 提供了 java.util.concurrent(JUC)包,内含线程安全队列、并发哈希表等工具,本质上是对"共享内存+锁"模型的封装与优化。
Go 中同样提供了这些低级同步原语,包括互斥锁(sync.Mutex)、读写锁(sync.RWMutex)、条件变量(sync.Cond)等。但 Go 的核心并发哲学并不鼓励将这些作为首选。
1.4 Go 的并发哲学:通过通信来共享内存
Go 提出了一套不同的并发范式:goroutine + channel。
goroutine 是 Go 的轻量级协程,channel 是 goroutine 之间传递消息的通道。Go 鼓励开发者通过 channel 在 goroutine 之间传递对共享数据的引用,而不是直接用锁来保护共享变量的访问。这种方式从设计上确保了在任意时刻只有一个 goroutine 真正拥有对某个数据的访问权。
这一理念被简洁地总结为:
不要通过共享内存来通信,而要通过通信来共享内存。
这不仅仅是一句口号,它体现了 Go 对并发安全性的一种结构化保证------通过语言层面的设计约束,将并发安全问题转化为消息传递的逻辑问题,从而让代码更容易推理和验证。
1.5 CSP 模型:Go 并发的理论根基
Go 的并发模型采用了 channel,其本质是 CSP(Communicating Sequential Processes,通信顺序进程)理论的一个变种。CSP 是英国计算机科学家 Tony Hoare 在 1978 年提出的一种描述并发系统的形式化语言,其核心思想是:通过进程之间的通信来协调行为,而不是依赖共享状态。
Go 选择 CSP 有两方面原因:其一,Google 内部工程师对这一理论已有较好的认知基础;其二,CSP 具有一种可以以最大程度正交化(orthogonal)的方式融入过程式编程语言的特性------它为语言提供额外的并发表达能力,同时不对语言的其他特性施加约束。
正因如此,Go 在保留传统过程式编程风格的同时,自然地加入了基于 CSP 的并发原语,而没有强迫开发者切换到完全不同的编程范式。
1.6 站在 goroutine 的视角思考并发
在 Java 中编写并发程序时,开发者必须在操作系统线程的层面上思考问题:线程的创建、线程的状态管理、线程的阻塞与唤醒,每一个细节都需要关注。
而在 Go 中,开发者不需要关心操作系统线程,只需站在 goroutine 和 channel 的层面上思考:
-
哪些任务可以并发执行?用 goroutine 来表达。
-
goroutine 之间如何传递数据和协调状态?用 channel 来实现。
-
在必要时,共享内存的访问同步?使用 sync 包的工具。
这一层次的抽象大大降低了并发编程的心智负担,也使得 Go 程序在面对高并发场景时既高效又易于维护。
1.7 Java 与 Go 并发模型的直观对比
为了让有 Java 背景的读者快速建立对比认知,我们在这里做一个简要的类比。
在 Java 中,创建并管理一个线程:
cs
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(linepublic static void main(String[] args) { Thread thread = new Thread(() -> { // 执行任务 System.out.println("线程执行中"); }); thread.start();}
在 Go 中,创建并运行一个 goroutine 只需一个关键字:
go
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(linefunc main() { go func() { // 执行任务 fmt.Println("goroutine 执行中") }()
// 等待 goroutine 执行完毕(实际代码应使用 sync.WaitGroup) time.Sleep(time.Second)}
Java 传统线程与 OS 线程是一对一的关系,每个线程的创建和切换成本较高,通常建议通过线程池来复用。JDK 21 引入了虚拟线程(Virtual Threads),实现了类似 goroutine 的 M:N 轻量级调度,但 Go 的并发模型在语言层面更原生、成本更低。Go 的 goroutine 由运行时调度,轻量到可以在单个程序中创建数十万个。
在同步方面,Java 常用 Semaphore、CountDownLatch 等工具;Go 使用 sync.WaitGroup:
go
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(linevar wg sync.WaitGroup
func main() { wg.Add(2)
go func() { defer wg.Done() fmt.Println("goroutine A 执行中") }()
go func() { defer wg.Done() fmt.Println("goroutine B 执行中") }()
fmt.Println("等待所有 goroutine 完成...") wg.Wait() fmt.Println("所有 goroutine 已完成")}
在数据传递方面,Java 通常需要使用并发安全的队列(如 LinkedBlockingQueue)来实现生产消费模型;Go 用 channel 原生表达这一概念,语义更清晰:
go
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(linevar wg sync.WaitGroup
func consumer(ch chan int) { for i := range ch { fmt.Println("消费:", i) } wg.Done()}
func main() { wg.Add(1) ch := make(chan int, 10) // 容量为 10 的有缓冲 channel
go consumer(ch)
for i := 1; i <= 100; i++ { ch <- i } close(ch) // 关闭 channel,通知消费者没有更多数据
wg.Wait() fmt.Println("主 goroutine 退出")}
1.8 本书的内容结构
本书内容按照下图所示的路径展开,从底层基础到高层实践,层层递进:
image.png
从下向上,首先通过基础篇讲解 Go 的线程模型、内存模型和 goroutine 的基础知识;然后深入讲解 Go 提供的低级同步原语------各类锁;接着讲解 Go 独特的通信原语------channel;最后通过实战案例,深刻理解"通过通信来共享内存"这一并发哲学的工程价值。
1.9 总结
本章介绍了 Go 语言并发编程的设计背景和核心哲学。Go 不是简单地在语言层面添加了线程支持,而是从 CSP 理论出发,以 goroutine 和 channel 为核心,构建了一套更简洁、更安全的并发编程模型。理解"通过通信来共享内存"这一理念,是学习 Go 并发编程的第一步,也是最重要的一步。