在Go语言开发里,启动了Goroutine之后发现终端什么都没打印,是新手和一部分老手都踩过的坑。这种现象通常不是fmt包坏了,而是程序执行顺序和并发模型没对齐。理解背后的调度规则,才能稳定地把协程里的日志打出来。

一、主函数提前退出导致协程来不及打印
Go程序的入口是main函数,当main函数所在的主Goroutine执行完毕,整个进程就会直接退出,而不会等待其他普通Goroutine运行结束。很多人在写demo时习惯这样启动协程:
package main
import "fmt"
func main() {
go func() {
fmt.Println("来自协程的打印")
}()
// main在这里直接结束,进程退出
}
上面这段代码在绝大多数机器上都不会输出任何内容。因为主Goroutine在启动子协程后立刻返回,操作系统把进程回收了,子协程内部的fmt.Println还没有被调度到。
有人会发现偶尔加一句time.Sleep就能打印出来,那只是因为主线程多睡了会儿,子协程碰巧被调度并执行完。这种方式不可靠,sleep时间长短依赖机器负载,不能作为正式方案。
二、使用同步机制保证打印可见
要让协程里的输出稳定出现,核心思路是让main等待子协程结束。最基础的做法是用channel做信号通知:
package main
import "fmt"
func main() {
done := make(chan bool)
go func() {
fmt.Println("来自协程的打印")
done <- true
}()
<-done // 阻塞等待协程完成
}
channel的接收操作会阻塞主Goroutine,直到子协程发送完成,这样打印一定会被执行。这种方式逻辑清晰,适合少量协程的场景。
如果协程数量多,官方更推荐sync.WaitGroup。它内部维护一个计数器,Add增加任务数,Done减少,Wait阻塞到计数归零:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("WaitGroup管理的协程打印")
}()
wg.Wait()
}
WaitGroup比channel更轻量,也避免了为每一个协程都建通道的繁琐。在并发打印日志、批量任务处理时,它是工程里最常用的同步手段。
三、Goroutine内panic被静默吸收
另一个隐蔽原因是协程里发生了panic,但因为没有recover,整个协程崩溃,后面的打印自然不会执行,而主流程可能毫无感知。看下面例子:
package main
import "fmt"
func main() {
done := make(chan bool)
go func() {
defer func() { done <- true }()
var s []int
fmt.Println(s[0]) // 这里panic,后面的打印没了
fmt.Println("这行不会输出")
}()
<-done
}
子协程在访问空切片时panic,导致后续语句中止。虽然我们用channel等到了协程结束,但终端上只看到崩溃前的部分,或者什么业务日志都没有。
正确的做法是在协程入口用defer加recover,把错误转成普通日志,既保护协程不连累整体,也能把问题打印出来:
package main
import (
"fmt"
"log"
)
func main() {
done := make(chan bool)
go func() {
defer func() {
if r := recover(); r != nil {
log.Println("协程发生异常:", r)
}
done <- true
}()
var s []int
fmt.Println(s[0])
fmt.Println("这行不会输出")
}()
<-done
}
加上recover之后,panic会被捕获并记录,程序不再悄悄吞掉错误,排查打印缺失的问题时也多了一条线索。
四、标准输出与日志库混用误区
有些项目用了第三方日志库,同时又在协程里直接fmt.Println,结果在某些环境下只有日志库的内容可见。这是因为部分日志库内部带缓冲或异步写文件,而fmt.Print直接写标准输出。如果主进程退出过快,缓冲没刷盘,看起来就像协程没打印。
建议统一输出通道:要么全部走标准库log,要么全部用同一个日志组件,并在退出前调用对应的Sync或Flush方法。这样无论是否在协程里,输出行为都一致,不会因缓冲差异造成误判。
| 方案 | 稳定性 | 适用场景 |
|---|---|---|
| time.Sleep | 低,靠运气 | 临时本地验证 |
| channel等待 | 高 | 少量协程通信 |
| WaitGroup | 高 | 多协程批量任务 |
| recover+日志 | 高 | 防panic吞错误 |
综合来看,Goroutine打印不出内容,本质都绕不开进程生命周期和调度时机。用WaitGroup或channel把主流程卡住,再配合recover兜底,基本能覆盖日常遇到的所有空白终端场景。