Go 语言入门系列(五):并发编程——goroutine、channel 与同步
本篇目标:掌握 Go 最被称道的语言级并发能力。核心哲学一句话:不要通过共享内存来通信,而要通过通信来共享内存。
一、goroutine
在函数调用前加 go,就在新的 goroutine 中运行。它是运行时管理的轻量级线程,创建成本极低,开几万个也没问题:
func say(s string) {
for i := 0; i < 3; i++ {
fmt.Println(s)
time.Sleep(100 * time.Millisecond)
}
}
go say("world") // 新 goroutine
say("hello") // 当前 goroutine
注意:main 返回时所有 goroutine 立即终止,不会等它们跑完。所以上面的例子可能看不到 "world" 全部输出。
二、channel
channel 是 goroutine 之间通信的管道:
ch := make(chan string)
go func() {
ch <- "来自 goroutine 的消息" // 发送
}()
msg := <-ch // 接收,阻塞等待
无缓冲 vs 有缓冲:
- 无缓冲(
make(chan int)):发送和接收同步,发送方阻塞直到有人接收 - 有缓冲(
make(chan int, 10)):缓冲区未满时发送不阻塞
关闭与遍历:
close(ch) // 由发送方关闭,接收方不关
for v := range ch { // 通道关闭后自动结束循环
fmt.Println(v)
}
v, ok := <-ch // ok 为 false 表示通道已关闭且无数据
三、select 多路复用
select 同时等待多个 channel,哪个就绪走哪个:
select {
case msg := <-ch1:
fmt.Println("收到:", msg)
case ch2 <- 42:
fmt.Println("发送成功")
case <-time.After(time.Second):
fmt.Println("超时")
default:
fmt.Println("都没就绪,不阻塞")
}
time.After 做超时,default 实现非阻塞尝试。这是并发控制的常用组合。
四、sync 包
channel 适合传数据,但有时只需同步状态,sync 更直接:
WaitGroup——等一组 goroutine 全部完成:
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println(n)
}(i)
}
wg.Wait() // 阻塞直到计数归零
Mutex——保护共享数据:
var (
mu sync.Mutex
sum int
)
mu.Lock()
sum++
mu.Unlock()
Once——保证只执行一次,适合初始化:
var once sync.Once
once.Do(func() { /* 只执行一次 */ })
五、context:超时、取消与传值
context 是控制 goroutine 生命周期的标准方式,尤其适合请求链路:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case result := <-doWork(ctx):
fmt.Println(result)
case <-ctx.Done():
fmt.Println("超时或被取消:", ctx.Err())
}
WithCancel 手动取消,WithTimeout 超时取消,WithValue 传请求级数据(只放必要的元数据,别滥用)。
六、常见并发陷阱
1. 循环变量捕获——旧版 Go 中,goroutine 里直接用循环变量会全部拿到最后一个值。解决:作为参数传入。
for i := 0; i < 3; i++ {
go func(n int) { fmt.Println(n) }(i) // 正确
}
(Go 1.22 起循环变量每轮独立,但显式传参仍是好习惯。)
2. goroutine 泄漏——goroutine 阻塞在无人接收的 channel 上永不退出。确保每条路径都有退出机制(context 或 close)。
3. 数据竞争——多个 goroutine 无保护地读写同一变量。用 go run -race 检测:
$ go run -race main.go
竞态检测器会报告冲突位置,开发阶段务必跑。
七、小结
go开 goroutine,main退出会杀掉所有 goroutine- channel 通信,无缓冲同步、有缓冲解耦;发送方负责
close select+time.After做超时和非阻塞sync.WaitGroup/Mutex/Once处理同步context管生命周期,陷阱用-race兜底
练习:写一个函数并发请求 3 个模拟接口(time.Sleep 模拟耗时),用 select + context.WithTimeout 在 2 秒内收集结果,超时则返回错误。
下一篇讲工程实践:测试、泛型与项目组织——从"会写代码"走向"能维护项目"。

浙公网安备 33010602011771号