Appearance
理解 Go 并发哲学:通过通信来共享内存
引言
在 Go 语言的并发语境中,有一句广为流传、却常被误解的话:
不要通过共享内存来通信,而应该通过通信来共享内存。
这句话并不是对 mutex、atomic 等同步原语的否定,而是一种并发设计哲学。它关注的核心不是“能不能共享内存”,而是如何管理状态、表达所有权以及降低并发系统的复杂度。
本文将从设计动机出发,结合具体代码示例,系统性地解释这句话在工程实践中的真实含义。
一、传统并发模型:共享内存 + 锁
在多数语言中,并发的默认模型是:
- 多个线程 / goroutine
- 访问同一块共享内存
- 通过锁保证一致性
示例:使用 mutex 保护共享状态
go
var (
counter int
mu sync.Mutex
)
func inc() {
mu.Lock()
counter++
mu.Unlock()
}
func main() {
for i := 0; i < 1000; i++ {
go inc()
}
time.Sleep(time.Second)
fmt.Println(counter)
}这段代码是正确的。但它有一个明显的特征:
- 正确性依赖于“所有访问 counter 的地方都必须记得加锁”
- 并发约束存在于程序员的认知中,而不是结构中
在系统规模变大后,这种隐式约束会成为主要风险来源。
二、Go 的主张:状态应当被拥有,而不是被围观
Go 的并发哲学试图回答一个更根本的问题:
这份数据,究竟属于谁?
在“通过通信来共享内存”的模型中:
- 数据在 goroutine 之间移动
- 所有权随通信发生转移
- 同一时刻,数据只被一个 goroutine 持有
示例:通过 channel 串行化状态访问
go
func counterServer(in <-chan int, out chan<- int) {
counter := 0
for {
select {
case v := <-in:
counter += v
case out <- counter:
}
}
}
func main() {
in := make(chan int)
out := make(chan int)
go counterServer(in, out)
for i := 0; i < 1000; i++ {
in <- 1
}
fmt.Println(<-out)
}这里的关键变化是:
counter不再是共享变量- 它被封装在一个 goroutine 内部
- 外界只能通过 channel 与之交互
这使得并发安全性从“人为约定”变成了“结构保证”。
三、channel 的本质:并发协议,而非简单队列
在工程上,channel 的价值远不止“传值”。
一个 channel 隐含地定义了:
- 数据流向
- 同步关系(阻塞 / 缓冲)
- 背压机制
- 生命周期边界
示例:使用 channel 表达工作流
go
type Job struct {
ID int
}
type Result struct {
ID int
}
func worker(jobs <-chan Job, results chan<- Result) {
for job := range jobs {
results <- Result{ID: job.ID}
}
}
func main() {
jobs := make(chan Job)
results := make(chan Result)
go worker(jobs, results)
jobs <- Job{ID: 1}
close(jobs)
fmt.Println(<-results)
}在这个模型中:
Job的所有权通过 channel 明确转移- worker 不需要关心锁
- 调度顺序天然由 channel 决定
并发控制被提升为架构层面的设计。
四、这句话并不排斥共享内存
需要明确的是:
Go 并没有禁止共享内存。
以下场景中,直接共享内存是合理的:
- 原子计数器(
sync/atomic) - 高性能缓存
- 极端性能敏感的热路径
示例:合理使用 atomic
go
var total int64
func inc() {
atomic.AddInt64(&total, 1)
}这里的选择是基于性能与复杂度的权衡,而不是哲学立场。
Go 的那句名言,更像是一条设计优先级提醒:
如果你已经在为锁的边界和顺序焦虑,说明可以考虑用通信模型重新审视设计。
五、工程化理解
在成熟的 Go 并发系统中,常见模式是:
- goroutine 持有私有状态
- channel 作为唯一交互入口
- 少量、可控的共享内存用于性能关键点
这种结构带来的收益包括:
- 更强的可读性
- 更低的心智负担
- 更容易推导正确性
结语
“不要通过共享内存来通信,而应该通过通信来共享内存”并不是一条强制规则,而是一种并发设计的价值取向。
它鼓励开发者:
- 用数据流替代状态争夺
- 用结构约束代替人为约定
- 用架构设计降低并发复杂度
在这个意义上,它不是关于 channel,而是关于如何构建可长期维护的并发系统。