
go程序中goroutine意外退出,往往是因为main函数过早结束导致整个进程退出,而非goroutine自身逻辑错误;本文通过分析典型channel通信案例,详解如何使用sync.waitgroup实现可靠同步,并修正消费端逻辑缺陷。
go程序中goroutine意外退出,往往是因为main函数过早结束导致整个进程退出,而非goroutine自身逻辑错误;本文通过分析典型channel通信案例,详解如何使用sync.waitgroup实现可靠同步,并修正消费端逻辑缺陷。
你的Go程序只输出了9次“url added”(索引0–8)就停止,根本原因并非addUrls函数出错,而是main函数执行完毕后立即退出进程,所有正在运行的goroutine(包括尚未完成的addUrls和仅执行了一次的consumeUrls)被强制终止。
在原始代码中:
-
go addUrls(urlHolder)启动协程后,main紧接着调用time.Sleep(100ms)—— 这段休眠远不足以让5000次带10ms延迟的写入完成(理论需50秒); - 随后启动
consumeUrls,但它仅从channel接收一次就返回,未循环消费; - 最后
fmt.Printf("Done")执行完毕,main函数退出,整个程序终结。
✅ 正确做法:使用 sync.WaitGroup 同步生命周期
WaitGroup 是Go标准库中专为等待一组goroutine完成而设计的同步原语,避免依赖不可靠的time.Sleep:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
urlHolder := make(chan string, 5000)
var wg sync.WaitGroup
// 启动生产者,计数+1
wg.Add(1)
go func() {
defer wg.Done() // 完成时减1
addUrls(urlHolder)
}()
// 启动消费者,计数+1
wg.Add(1)
go func() {
defer wg.Done()
consumeUrls(urlHolder)
}()
// 等待所有goroutine完成
wg.Wait()
fmt.Println("Done")
}
func addUrls(urlHolder chan string) {
myurl := "https://example.com/"
for i := 0; i < 5000; i++ {
urlHolder <- myurl
fmt.Printf(" %d url added\n", i)
time.Sleep(10 * time.Millisecond)
}
// 生产完成后关闭channel,通知消费者可安全退出
close(urlHolder)
}
func consumeUrls(urlHolder chan string) {
count := 0
for url := range urlHolder { // 使用range自动处理关闭信号
count++
fmt.Printf("Pulled url %d: %s\n", count, url)
time.Sleep(20 * time.Millisecond)
}
}⚠️ 关键改进说明:
-
sync.WaitGroup替代time.Sleep:精确控制main退出时机,确保所有goroutine自然完成; -
close(urlHolder)+for range:显式关闭channel,使消费者能通过range自动感知结束,避免死锁或漏消费; -
匿名goroutine封装:便于
defer wg.Done()统一管理完成状态; -
移除全局变量
urlCount:改用局部变量提升并发安全性(原代码中urlCount++无同步保护,存在竞态风险)。
? 额外提醒:
- 原
consumeUrls函数仅接收一次即返回,属于逻辑缺陷,非同步问题——即使main不退出,它也只会打印1个URL; - channel容量为5000虽能暂存全部URL,但若消费者过慢,生产者仍会在第5001次写入时阻塞(本例因
main早退未暴露该问题); - 实际项目中,应结合
context控制超时与取消,而非依赖固定Sleep。
遵循上述模式,即可写出健壮、可预测的Go并发程序。
立即学习“go语言免费学习笔记(深入)”;


















