这是 Go 最核心、最有魅力的部分。Go 把并发编程变得像写普通代码一样简单。你不需要懂线程、锁、信号量这些复杂概念,只需要理解两个东西:协程(goroutine) 和 通道(channel)。
1. 并发 vs 并行
先搞清这两个概念:
- 并发(Concurrency):一个人同时应付多件事。比如边做饭边接电话——你在两件事之间来回切换,看起来像同时在做。
- 并行(Parallelism):多个人同时各做各的事。比如两个厨师同时炒两道菜——真正的同时。
Go 的协程是并发的——多个协程在同一个 CPU 核心上轮流执行。如果有多核,Go 也会让协程真正并行运行。
💡 要点速记:并发是"同时处理多件事",并行是"同时做多件事"。Go 协程默认并发,多核时自动并行。
2. 协程(Goroutine)
2.1 启动一个协程
在函数调用前加 go,就变成协程了。就像喊了一声"你去做",自己不等着结果:
gofunc sayHello(name string) {
fmt.Printf("你好,%s!\n", name)
}
func main() {
go sayHello("协程") // 启动协程,不会阻塞
sayHello("主函数") // 主函数自己也在跑
time.Sleep(time.Second) // 等一下,否则主函数结束程序就退了
}
输出可能是(顺序不固定):
你好,主函数!
你好,协程!
2.2 协程的本质
协程比操作系统线程轻量得多:
- 线程通常占 1MB 栈内存,协程只占 2KB(可动态增长)
- 创建线程需要系统调用,创建协程只是函数调用
- 一台机器轻松跑几十万个协程
2.3 匿名函数协程
gogo func(msg string) {
fmt.Println(msg)
}("我来自协程")
2.4 等协程完成
time.Sleep 不靠谱(你怎么知道要等多久?)。用 sync.WaitGroup 才是正道:
govar wg sync.WaitGroup
func worker(id int) {
defer wg.Done() // 完成时通知
fmt.Printf("工人 %d 开始干活\n", id)
time.Sleep(time.Millisecond * time.Duration(rand.Intn(500)))
fmt.Printf("工人 %d 干完了\n", id)
}
func main() {
for i := 1; i <= 5; i++ {
wg.Add(1) // 每启动一个协程,计数+1
go worker(i)
}
wg.Wait() // 等所有协程完成
fmt.Println("全部完成")
}
💡 要点速记:
go 函数()启动协程- 协程超轻量,可以创建很多
- 用
sync.WaitGroup等待多个协程完成,不要用time.Sleep - 主函数退出 = 程序退出 = 所有协程被杀
3. 通道(Channel)
3.1 为什么需要通道?
多个协程干活,怎么互相传递数据?用共享变量?那得加锁,太麻烦。Go 的方案是:不要通过共享内存来通信,而要通过通信来共享内存。
通道就像一根管子——一头塞东西进去,另一头拿出来。先进先出,线程安全。
3.2 创建和使用通道
go// 创建一个传输 int 的通道
ch := make(chan int)
// 协程1:发送数据
go func() {
ch <- 42 // 把 42 塞进通道(箭头方向表示数据流向)
}()
// 协程2(主函数):接收数据
value := <-ch // 从通道取数据
fmt.Println("收到:", value) // 输出:收到:42
ch <- value 发送,<-ch 接收,value := <-ch 接收并赋值。
3.3 通道的阻塞性
无缓冲通道是同步的——发送方和接收方必须同时就绪:
goch := make(chan string)
go func() {
ch <- "消息" // 如果没人接收,这里会一直等待
}()
msg := <-ch // 如果没人发送,这里也会一直等待
这就像打电话——双方都得接听才能通话。
3.4 带缓冲的通道
goch := make(chan int, 3) // 缓冲区大小 3
ch <- 1 // 不阻塞,缓冲区有空间
ch <- 2 // 不阻塞
ch <- 3 // 不阻塞
// ch <- 4 // 阻塞!缓冲区满了,必须等有人取走
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
带缓冲的通道像邮箱——你可以塞信进去,收信人不用立刻取。满了就塞不下了,空了就取不出来了。
💡 要点速记:
make(chan T)无缓冲,make(chan T, n)缓冲区大小 n- 无缓冲通道:发送和接收必须同时就绪,否则阻塞
- 带缓冲通道:缓冲区满时发送阻塞,缓冲区空时接收阻塞
- 箭头
<-指向数据流向:ch <-发送,<- ch接收
4. 关闭通道和 range 遍历
4.1 关闭通道
发送方可以关闭通道,告诉接收方"没有更多数据了":
goch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // 发送完毕,关闭通道
}()
// 接收方可以用 range 遍历通道
for num := range ch {
fmt.Println(num) // 输出 0 1 2 3 4
}
注意:
- 只有发送方应该关闭通道,接收方不要关
- 关闭后的通道不能再发送数据(会 panic)
- 关闭后的通道还可以接收(返回零值和 false)
- 向已关闭的通道发送数据会 panic
4.2 检测通道是否关闭
govalue, ok := <-ch
if !ok {
fmt.Println("通道已关闭")
break
}
💡 要点速记:
- 发送方用
close(ch)关闭通道 for x := range ch自动在通道关闭后退出循环value, ok := <-ch中ok为 false 表示通道已关闭- 不要关闭已经关闭的通道,不要向已关闭的通道发送
5. select 语句
5.1 多路复用
select 就像前台接待员——同时盯着多部电话,哪部响了接哪部:
goch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "来自通道1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "来自通道2"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("收到:", msg1)
case msg2 := <-ch2:
fmt.Println("收到:", msg2)
}
}
输出(1秒后先收到通道1,2秒后收到通道2):
收到:来自通道1
收到:来自通道2
5.2 select 的规则
- 多个 case 同时就绪,随机选一个执行
- 没有 case 就绪,阻塞等待
- 加了
default,没有 case 就绪时执行 default(不阻塞)
5.3 超时控制
用 select + time.After 实现超时:
goselect {
case result := <-ch:
fmt.Println("收到结果:", result)
case <-time.After(3 * time.Second):
fmt.Println("超时了!3秒内没收到数据")
}
time.After 返回一个通道,在指定时间后会发送当前时间。就像设置一个闹钟——到点了就触发。
5.4 定时器(Ticker)
time.NewTicker 创建周期性触发的定时器:
goticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for i := 0; i < 5; i++ {
<-ticker.C // 每秒收到一次
fmt.Println("滴答", i+1)
}
💡 要点速记:
select同时监听多个通道,哪个先有数据就处理哪个- 多个同时就绪时随机选一个
time.After(duration)实现超时控制time.NewTicker(duration)创建周期定时器
6. 协程中的 panic 恢复
如果协程里的 panic 没人 recover,整个程序会崩溃。所以每个重要协程都应该自己兜底:
6.1 safeGo:安全的协程启动器
gofunc safeGo(fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Println("协程 panic 恢复:", r)
}
}()
fn()
}()
}
safeGo 封装了 go 关键字,自动给每个协程加上 recover 保护。协程里即使 panic,也不会拖垮整个程序。
6.2 必须配合 WaitGroup 使用
光有 safeGo 还不够——main 退出后所有协程会被强制杀死,recover 来不及输出。必须配合 WaitGroup 等 goroutine 跑完:
gofunc main() {
var wg sync.WaitGroup
wg.Add(1)
safeGo(func() {
defer wg.Done() // goroutine 里 Done,不是 main 里
panic("出大事了") // 不会崩溃整个程序
})
wg.Wait() // 等 goroutine 完成再退出
}
6.3 常见错误
错误 1:wg.Done() 写在 main 里
go// ❌ main 自己把计数器清零了,Wait 立刻返回,goroutine 还没跑就被杀
safeGo(func() {
panic("出大事了")
})
wg.Done() // 包工头替工人划考勤,以为没人了就下班
wg.Wait()
错误 2:不写 wg.Done()
go// ❌ 计数器永远不归零,Wait 永远阻塞 → 死锁
safeGo(func() {
panic("出大事了")
// 没有 defer wg.Done()
})
wg.Wait() // 计数器还是 1,天荒地老地等
错误 3:不用 WaitGroup
go// ❌ main 退出太快,goroutine 来不及执行就被杀
safeGo(func() {
panic("出大事了")
})
// 没有 wg.Wait(),main 直接结束,程序退出
6.4 完整执行流程
gofunc safeGo(fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Println("协程 panic 恢复:", r)
}
}()
fn()
}()
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
safeGo(func() {
defer wg.Done()
panic("出大事了")
})
wg.Wait()
}
执行时序:
输出:
2026/06/01 15:30:00 协程 panic 恢复: 出大事了
💡 要点速记:
- 每个协程应该自己 recover panic,否则整个程序会崩
safeGo= 带安全网的go,panic 不会拖垮进程wg.Done()必须写在 goroutine 里面,用defer兜底wg.Add()和wg.Done()必须配对,多一个 Add 死锁,多一个 Done panic- defer 执行顺序:后注册先执行,所以
wg.Done()在recover()之前执行
7. 常见并发模式
7.1 Worker Pool(工人池)
固定数量的工人处理任务,避免无限制创建协程:
gofunc worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("工人 %d 处理任务 %d\n", id, job)
time.Sleep(time.Millisecond * 500) // 模拟工作
results <- job * 2
}
}
func main() {
jobs := make(chan int, 10)
results := make(chan int, 10)
// 启动 3 个工人
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 发送 5 个任务
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs) // 任务发完了
// 收集结果
for r := 1; r <= 5; r++ {
fmt.Println("结果:", <-results)
}
}
7.2 限制并发数
用带缓冲的通道做信号量,限制同时运行的协程数量:
govar sem = make(chan struct{}, 5) // 最多 5 个并发
func process(url string) {
sem <- struct{}{} // 占一个位置(满了就等)
defer func() { <-sem }() // 完成后释放位置
// 处理请求...
fmt.Println("处理:", url)
time.Sleep(time.Second)
}
7.3 通道多路复用(Fan-in)
多个通道的数据汇聚到一个通道:
gofunc fanIn(ch1, ch2 <-chan string) <-chan string {
merged := make(chan string)
go func() {
for msg := range ch1 { merged <- msg }
}()
go func() {
for msg := range ch2 { merged <- msg }
}()
return merged
}
7.4 超时模式
给任何操作加超时保护:
gofunc doWithTimeout(timeout time.Duration) (int, error) {
resultCh := make(chan int)
go func() {
// 可能很慢的操作
time.Sleep(2 * time.Second)
resultCh <- 42
}()
select {
case result := <-resultCh:
return result, nil
case <-time.After(timeout):
return 0, fmt.Errorf("操作超时(%v)", timeout)
}
}
7.5 Pipeline(管道)
数据像流水线一样经过多个处理阶段:
go// 阶段1:生成数据
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
// 阶段2:平方
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
// 串联:generate → square → 打印
func main() {
ch := generate(2, 3, 4)
ch = square(ch)
for result := range ch {
fmt.Println(result) // 4, 9, 16
}
}
💡 要点速记:
- Worker Pool:固定工人处理任务,避免协程爆炸
- 信号量模式:带缓冲通道限制并发数
- Fan-in:多个通道汇聚到一个
- Pipeline:数据经过多个阶段串行处理
8. 高级模式概览
以下模式初学时了解概念即可,遇到实际需求再深入研究。
8.1 Futures 模式
"我先去干活,结果回头来拿"——启动协程计算,返回一个"未来结果"通道:
gofunc asyncCompute() <-chan int {
future := make(chan int)
go func() {
result := heavyComputation() // 很慢的计算
future <- result
}()
return future // 立刻返回,不等计算完
}
// 使用
f := asyncCompute()
// 可以做别的事...
result := <-f // 需要结果时再取
8.2 多核并行计算
gofunc parallelSum(data []int) int {
n := runtime.NumCPU()
size := (len(data) + n - 1) / n
results := make(chan int, n)
for i := 0; i < n; i++ {
go func(start int) {
sum := 0
end := start + size
if end > len(data) { end = len(data) }
for j := start; j < end; j++ {
sum += data[j]
}
results <- sum
}(i * size)
}
total := 0
for i := 0; i < n; i++ {
total += <-results
}
return total
}
8.3 限流(Leaky Bucket 漏桶)
控制请求速率,防止系统被冲垮:
gofunc rateLimiter(interval time.Duration) chan struct{} {
bucket := make(chan struct{}, 1)
go func() {
for {
bucket <- struct{}{}
time.Sleep(interval)
}
}()
return bucket
}
// 每 100ms 允许一个请求
limiter := rateLimiter(100 * time.Millisecond)
for i := 0; i < 10; i++ {
<-limiter // 等待令牌
fmt.Println("处理请求", i)
}
💡 要点速记:
- Futures:异步计算,需要时再取结果
- 多核并行:把数据分片,每个核心算一片,最后合并
- 限流:用定时器控制处理速率
9. 新手常见坑
- 协程泄漏:协程启动后永远阻塞在通道上,没人发送/接收,协程就永远挂着。确保通道最终会被关闭或发送数据。
- 死锁:两个协程互相等待对方发送/接收,谁也动不了。常见于无缓冲通道的"双方都没准备好"的情况。
- 向已关闭的通道发送:这会 panic。记住只有发送方关闭,且关闭后不要再发。
- range 遍历通道时没关闭:
for x := range ch会一直等数据,如果没人 close(ch),就永远阻塞。 - 闭包捕获循环变量:经典的坑——for 循环里启动协程,协程里用了循环变量,结果所有协程拿到的是同一个值:
go// 错误!所有协程都打印 3
for i := 0; i < 3; i++ {
go func() { fmt.Println(i) }()
}
// 正确:把 i 作为参数传入
for i := 0; i < 3; i++ {
go func(n int) { fmt.Println(n) }(i)
}
- 主函数退出导致协程被杀:主函数 return 或 os.Exit 后,所有协程立刻终止,不管它们是否完成。用 WaitGroup 或通道同步来等待。
- 无缓冲通道在同一个协程发收:
ch <- 1; x := <-ch在同一个协程会死锁——发送在等接收,但接收代码在发送后面,永远执行不到。必须分开到不同协程。 - select 空 case:
select {}会永远阻塞当前协程。有时候是有意为之(让主协程不退出),但别写错了。