Go语言学习第四天
- 1Go语言学习第四天本文
- 2Go语言学习第三天
- 3Go语言学习第二天
- 4Go语言学习第一天

Go 入门笔记(四):协程与信道
第四阶段开始学习 Go 最出名的特性——并发。两个练习文件:协程(goroutine)配合 WaitGroup,以及用信道(channel)在协程间传数据。所有代码均在 Go 1.27.2 下实际运行,这次还真的抓到了一个隐蔽的 bug,收获很大。
一、协程:用 go 关键字启动并发
从顺序执行到并发执行
练习模拟的是”发起 3 个请求,每个耗时 1 秒”。顺序执行的版本要 3 秒才能跑完:
// 顺序请求数据(已注释):总共约 3 秒// axios("1")// axios("2")// axios("3")Go 里把函数变成并发执行,只需要在调用前加 go:
func axios(id string, wait *sync.WaitGroup) { fmt.Println(id + "请求数据") time.Sleep(time.Second) fmt.Println("响应数据") wait.Done() // 完成一个,计数减一}
var wait sync.WaitGroupwait.Add(3) // 总共要等 3 个go axios("1", &wait)go axios("2", &wait)go axios("3", &wait)wait.Wait() // 阻塞在这里,直到计数归零
fmt.Println("获取数据结束")为什么需要 WaitGroup
main 函数返回时,程序直接退出,不会等任何协程。 如果没有 wait.Wait(),main 启动 3 个协程后立刻走完,协程还没来得及打印就被”连锅端”了。sync.WaitGroup 就是专门解决”等一组协程全部完成”的计数器:
Add(3):声明要等 3 个任务;- 每个协程干完活调用
Done():计数减一; Wait():阻塞,直到计数归零才放行。
注意 &wait 传的是指针——WaitGroup 内部维护计数,必须传地址才能让所有协程操作同一个计数器。
实际运行输出:
1请求数据3请求数据2请求数据响应数据响应数据响应数据获取数据结束两个观察:
- 启动顺序不等于执行顺序:
1 3 2的打印顺序是随机的,协程由 Go 调度器安排,谁先跑不一定,多跑几次顺序会变。 - 3 个 1 秒的任务几乎同时完成:三个”响应数据”扎堆出现,总耗时约 1 秒而不是 3 秒——这就是并发的意义。
小建议:实际项目里 wait.Done() 更稳妥的写法是 defer wait.Done() 放在函数开头,这样即使中途 return 或 panic 也不会漏掉计数。
二、信道:协程之间的数据通道
协程能并发干活了,下一个问题是:怎么把各个协程的结果收回来? 这就是信道(channel)的用途。
无缓冲信道的发送与接收
// 长度(容量)为零的信道,即无缓冲信道var moneyChan = make(chan int)信道用 make(chan 类型) 创建,操作符是 <-,方向决定读写:
moneyChan <- money // 发送:数据流向信道money := <-moneyChan // 接收:数据从信道流出练习场景:3 个协程各自”购买物品”花掉一笔钱,把金额通过信道发回 main 汇总:
func pay(id string, money int, wait *sync.WaitGroup) { fmt.Println(id + "购买物品") time.Sleep(time.Second) fmt.Println("购买完成") moneyChan <- money // 发送数据到信道 wait.Done()}无缓冲信道的特性是同步:发送方会阻塞到有人接收,接收方会阻塞到有人发送,两边必须”碰头”才能完成一次传输。
close 与 range:谁来收尾
main 里用 range 持续接收信道数据:
go func() { wait.Wait() close(moneyChan)}()
for money := range moneyChan { // 修正后的写法,详见下文 moneyList = append(moneyList, money)}fmt.Println("消费结束")range 信道 会一直接收,直到信道被 close 才结束循环。如果没人 close,range 会永远阻塞下去(协程都退出后还会报 deadlock)。但 close 的时机有讲究:必须等所有发送方发完才能关,否则往已关闭的信道发送会 panic。
代码里的解法是一个很经典的模式——单独起一个协程专门负责关闭:
- main 自己不能先
wait.Wait()再 range,那样 main 阻塞在 Wait 上时没人接收,发送方全卡住,死锁; - 所以把”等待+关闭”交给另一个协程,main 只管专心接收。
抓到一个 bug:range <-moneyChan
我最初写的代码其实是这样(多了个 <-):
for money := range <-moneyChan { // 错误写法! moneyList = append(moneyList, money)}它能编译、能运行、还不报错,但行为完全不对。我在临时环境把 moneyList 打印出来对比:
原代码(range <-moneyChan):[0 1 2 3 4]修正后(range moneyChan): [5 20 10]原因:range 后面的表达式是 <-moneyChan,这是”从信道接收一个值”,类型是 int。而 Go 1.22 起允许对整数 range(for i := range n 循环 0 到 n-1)。于是这行代码的真实执行过程是:
- 从信道接收一个金额(比如某个协程发的 5);
- 把这个 5 当成循环次数,
money依次等于 0、1、2、3、4; - 收集到的全是循环下标,真正的 5、10、20 全丢了。
这也解释了为什么运行时”消费结束”会抢在最后一个”购买完成”之前打印——main 只接收了一次就循环完走人了,根本没等其他协程。如果在 Go 1.22 之前,这种写法会直接编译报错,是 range-over-int 新特性让它变成了”安静的错误”。教训:range 信道时信道变量前不要加 <-。
另外一个小提醒:原代码里 moneyList 收集了数据却从没打印或使用,练习时可以加一行 fmt.Println(moneyList) 确认结果。
易错点与补充汇总
| 位置 | 说明 |
|---|---|
| 16 协程 | main 返回程序即退出,必须用 WaitGroup 等方式等待协程结束 |
| 16 协程 | WaitGroup 要传指针;Done() 建议用 defer 调用防遗漏 |
| 17 信道 | range <-moneyChan 是 bug:<- 会先接收出一个 int,Go 1.22+ 对 int 可 range,错误被静默掩盖;应为 range moneyChan |
| 17 信道 | range 信道靠 close 结束;往已关闭信道发送会 panic;应由发送方(全部发完后)关闭 |
| 17 信道 | “等所有协程完成后关闭信道”用单独协程 + WaitGroup 是经典模式 |
| 通用 | 协程执行顺序随机,输出先后不代表启动先后 |
总结
这一阶段的两个核心概念:go 关键字让函数并发执行,WaitGroup 解决”等待一组协程”;信道解决”协程间传数据”,无缓冲信道天然同步,range + close 是批量接收的标配写法。最深的教训来自那个 range <-moneyChan 的 bug——能编译、能运行、输出看起来还正常,只有打印真实数据才暴露问题。并发代码的验证不能只看”程序没崩”,一定要核对数据本身。
后续值得探索:带缓冲的信道(make(chan int, 3))、select 多路复用、以及 channel 关闭后接收方拿到的零值与 comma-ok 判断。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!




