当主 goroutine 陷入无限空循环 for{} 时,Go 运行时无法调度其他 goroutine 执行——即使它们已成功启动,也会永远“休眠”在就绪队列中,导致看似“未启动”的假象。
当主 goroutine 陷入无限空循环 `for{}` 时,go 运行时无法调度其他 goroutine 执行——即使它们已成功启动,也会永远“休眠”在就绪队列中,导致看似“未启动”的假象。
在 Go 并发模型中,goroutine 的执行依赖于 Go 调度器(Goroutine Scheduler)的主动调度。而调度器本身需要运行机会:它通常在以下时机介入——函数调用、channel 操作、系统调用、垃圾回收暂停点,或显式让出控制权(如 runtime.Gosched())。但一个纯 CPU 密集型的 for{} 循环
不触发任何上述事件
,它会持续独占当前 OS 线程(M),使调度器彻底“失声”。
这正是你代码中问题的核心:
尽管 go mygoroutine(...) 语句被正确解析、goroutine 已被创建并加入就绪队列(/sched/goroutines/runnable 指标将显示其存在),但由于主 goroutine 卡死在 for{},Go 运行时无法切换到其他 goroutine,因此 "In goroutine for file..." 日志永远不会打印。
?
验证提示
:可通过 Go 1.26 新增的运行时指标实时观测这一状态:
你会看到 /sched/goroutines/runnable > 0,而 /sched/goroutines/running 长期为 1(仅 main),证实 goroutine 就绪却无法运行。
✅ 正确做法:用 select{} 替代 for{}
select{} 是 Go 中实现“永久阻塞且让出调度权”的惯用模式。它不消耗 CPU,同时允许调度器持续工作:
⚠️ 注意:select{} 本身不会导致程序退出;它只是让 main goroutine 永久等待(无 case 可选),但此时其他 goroutine 可正常执行并完成。不过,若所有 goroutine 结束后 main 仍卡在 select{},程序将因“all goroutines are asleep — deadlock!”崩溃——这反而是 Go 的健康保护机制,提示你
缺少同步逻辑
。
✅ 生产级推荐:使用 sync.WaitGroup 显式等待
真正健壮的方案是明确协调生命周期,而非依赖阻塞原语:
? 关键总结
for{} ≠ “等待”,而是
调度器拒绝服务
;它是 Go 并发编程中最隐蔽的反模式之一。
select{} 是语义正确的永久阻塞方式,适用于守护型长期运行 goroutine(如监听 channel),但需配合退出机制。
sync.WaitGroup 或带缓冲 channel 是处理“启动 N 个任务并等待全部完成”的标准、可读、可维护方案。
始终牢记 Go 的并发哲学:
“不要通过共享内存来通信;而应通过通信来共享内存。”
—— 避免竞态的同时,也请信任调度器,别亲手把它锁死。
func main() {
file_names := get_file_names()
for _, file_name := range file_names {
log.Println("Now launching goroutine for file", file_name)
go mygoroutine(file_name) // ✅ 启动成功!但后续无调度机会
}
log.Println("Finished launching.")
for {} // ❌ 致命陷阱:永不释放 CPU,调度器无法运行
log.Println("Now exiting") // 永远不会执行
}import "runtime/metrics"
func printGoroutineStats() {
m := metrics.Read(metrics.All())
for _, v := range m {
if strings.HasPrefix(v.Name, "/sched/goroutines/") {
fmt.Printf("%s: %d\n", v.Name, v.Value.Uint64())
}
}
}func main() {
file_names := get_file_names()
for _, file_name := range file_names {
log.Println("Now launching goroutine for file", file_name)
go mygoroutine(file_name)
}
log.Println("Finished launching.")
select {} // ✅ 安全阻塞:调度器可自由调度其他 goroutine
}package main
import (
"log"
"sync"
"io/ioutil"
"strings"
)
const BASE_FILE_NAME = "abc_"
func mygoroutine(file_name string, wg *sync.WaitGroup) {
defer wg.Done()
log.Println("In goroutine for file", file_name)
}
func get_file_names() []string {
file_names := make([]string, 0)
files, _ := ioutil.ReadDir("./")
for _, file := range files {
if strings.HasPrefix(file.Name(), BASE_FILE_NAME) {
file_names = append(file_names, file.Name())
}
}
return file_names
}
func main() {
var wg sync.WaitGroup
file_names := get_file_names()
for _, file_name := range file_names {
log.Println("Now launching goroutine for file", file_name)
wg.Add(1)
go mygoroutine(file_name, &wg)
}
log.Println("Finished launching.")
wg.Wait() // ✅ 等待所有 worker 完成
log.Println("All goroutines finished. Now exiting.")
}