并发是 Go 语言最大的卖点之一:不用显式开线程、不用记复杂的线程 API,在函数调用前加一个 go 关键字,代码就并发跑起来了。这个轻量的执行单元就叫 goroutine。本篇带你从零学会启动、等待和协调 goroutine。
什么是 goroutine
goroutine 是由 Go 运行时调度的轻量级"用户态线程"。它和操作系统线程的区别在于:启动成本极低(初始栈只有几 KB),一个程序里同时跑成千上万个 goroutine 是家常便饭。你可以把它理解成"超轻量的并发任务"。
用 go 关键字启动并发
在普通函数调用前加上 go,这个函数就会在一个新的 goroutine 里执行,主流程不必等它结束:
package main
import (
"fmt"
"time"
)
func say(name string) {
for i := 1; i <= 3; i++ {
fmt.Println(name, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go say("goroutine") // 新 goroutine 里并发执行
say("main") // 主 goroutine 自己执行
}
输出会交错出现,类似:
main 1
goroutine 1
main 2
goroutine 2
main 3
goroutine 3
注意一个经典陷阱:把 go say(...) 的输出删掉后,程序可能什么都打印不出来就结束了——因为 main 一返回,所有还没跑完的 goroutine 会被直接终止,没人会等它们。这就引出下一个话题:如何等待。
用 sync.WaitGroup 等待所有任务完成
最常用的等待工具是 sync.WaitGroup:启动前 Add(1) 计数加一,任务结束时 Done() 计数减一,主流程 Wait() 阻塞到计数归零。
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 函数返回时计数减一
fmt.Println("工人", id, "完成工作")
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg) // 注意传指针,保证大家共用同一个 wg
}
wg.Wait()
fmt.Println("全部完工")
}
输出顺序可能不同,但"全部完工"一定最后打印。两个细节:wg.Add(1) 必须在 go 语句之前调用;传给 goroutine 的必须是 &wg(指针),否则每个 goroutine 会拿到一份拷贝,Wait 永远等不到归零。
用 channel 在 goroutine 之间传数据
goroutine 之间不靠共享变量通信(容易出数据竞争),Go 推荐用 channel(通道)传递数据。无缓冲 channel 的收发会互相等待,天然形成同步:
package main
import "fmt"
func producer(ch chan int) {
for i := 1; i <= 3; i++ {
ch <- i // 发送数据进通道
}
close(ch) // 发送完毕,关闭通道
}
func main() {
ch := make(chan int)
go producer(ch)
for v := range ch { // 通道关闭后循环自动结束
fmt.Println("收到:", v)
}
}
输出依次是 收到: 1、收到: 2、收到: 3。range ch 会一直接收,直到通道被 close,这个模式在"生产者-消费者"场景里非常好用。
新手最容易踩的坑
- main 提前退出:忘了等待,goroutine 还没跑完程序就结束了。用
WaitGroup或 channel 收尾。 - 循环变量捕获:旧版本 Go 中直接
go func(){ 用到 i }()可能共享同一个i,保险起见把i作为参数传入。 - 数据竞争:多个 goroutine 同时读写同一个变量没有同步,结果不可复现。能传 channel 就传 channel,实在要用共享变量再加
sync.Mutex。
小结
- goroutine 是 Go 运行时调度的轻量并发单元,函数调用前加
go即可启动。 main返回会终止一切,等待任务用sync.WaitGroup:Add在启动前、Done配合defer、Wait阻塞收尾。- goroutine 之间优先用 channel 传数据,
range ch+close(ch)是标准收发模式。 - 并发不等于安全,多个 goroutine 写同一变量需要 channel 或
sync.Mutex保护。
channel 那一段讲得太清楚了,range + close 的收发模式终于看明白了。作者后面能安排一篇讲 select 多路复用吗?