
go程序中启动goroutine后主函数过早退出,导致子协程无机会运行;需通过sync.waitgroup、channel等机制显式等待协程完成,而非依赖runtime.gosched()这类非确定性调度让步。
在Go语言中,go关键字用于启动一个新的goroutine,它会在后台并发执行,但不会阻塞当前执行流。这意味着:一旦main()函数执行完毕并返回,整个程序立即终止——无论其他goroutine是否仍在运行或尚未开始。
观察原始代码:
func main() {
go say("world") // 启动新goroutine,但不等待
say("hello") // 主goroutine同步执行
} // ← main函数结束 → 程序退出!"world"几乎必然被丢弃此处的问题本质不是“goroutine没被调度”,而是程序生命周期管理缺失:main()函数在say("hello")返回后立刻退出,操作系统回收进程资源,所有未完成的goroutine(包括刚启动的say("world"))被强制终止。因此输出仅见5次hello,world完全不可见。
⚠️ 注意:runtime.Gosched()并非解决方案
虽然添加runtime.Gosched()(如问题中修改版)偶尔能让world出现,但这属于竞态依赖调度器行为,不可靠且违背Go并发设计哲学。Gosched()仅建议调度器切换到其他goroutine,但不保证目标goroutine一定被执行,更不保证执行完成。该做法在生产环境绝对禁止。
✅ 正确做法:使用显式同步原语确保主goroutine等待子goroutine完成。
立即学习“go语言免费学习笔记(深入)”;
推荐方案一:sync.WaitGroup(最常用、语义清晰)
WaitGroup专为“等待一组goroutine结束”而设计,通过计数器+阻塞等待实现可靠同步:
package main
import (
"fmt"
"sync"
)
func say(s string) {
for i := 0; i < 5; i++ {
fmt.Println(s)
}
}
func main() {
var wg sync.WaitGroup
// 增加计数器:1个goroutine将运行say("world")
wg.Add(1)
go func() {
defer wg.Done() // 执行完毕后递减计数器
say("world")
}()
// 主goroutine同步执行say("hello")
say("hello")
// 阻塞等待所有Add对应的Done调用完成
wg.Wait()
}✅ 关键点: wg.Add(1) 必须在go语句之前调用(避免竞态); defer wg.Done() 确保函数退出时准确计数; wg.Wait() 在主goroutine中阻塞,直到计数器归零。
推荐方案二:channel(适用于需要传递结果或复杂协调的场景)
当goroutine需返回数据、或存在依赖关系时,channel更自然:
package main
import "fmt"
func say(s string, done chan<- bool) {
for i := 0; i < 5; i++ {
fmt.Println(s)
}
done <- true // 通知完成
}
func main() {
done := make(chan bool, 2) // 缓冲通道,避免发送阻塞
go say("world", done)
go say("hello", done) // 注意:此处也应并发执行,否则仍无法体现并发性
// 等待两个goroutine均完成
<-done
<-done
}⚠️ 注意事项:
- 若done是无缓冲channel,则发送操作会阻塞,直到有接收者——因此必须确保main中
- 使用带缓冲的channel(如make(chan bool, 2))可避免死锁,但逻辑上仍需匹配发送/接收次数;
- 本例中say("hello")若保持同步调用,则失去并发意义,应同样用go启动。
总结
- 根本原因:Go程序在main()函数返回时立即退出,不等待其他goroutine;
- 错误认知:runtime.Gosched()不是同步机制,不能替代等待逻辑;
-
最佳实践:
- 简单等待 → 用 sync.WaitGroup;
- 需要通信/结果传递 → 用 channel;
- 绝对避免依赖调度器行为或time.Sleep等不精确手段;
- 额外提醒:始终检查go语句前后是否存在竞态(如共享变量未加锁)、WaitGroup.Add调用时机是否安全。
掌握goroutine生命周期与同步机制,是写出健壮Go并发程序的第一课。










