
goroutine 启动后立即进入就绪队列,但其实际执行时间不可预测;go 运行时会在函数调用、系统调用或循环等位置插入调度点(yield points),但这是实现细节,程序逻辑绝不能依赖此类非确定性行为。
goroutine 启动后立即进入就绪队列,但其实际执行时间不可预测;go 运行时会在函数调用、系统调用或循环等位置插入调度点(yield points),但这是实现细节,程序逻辑绝不能依赖此类非确定性行为。
在 Go 中,go 关键字用于启动一个新 goroutine,它并非立即抢占式执行,也不保证与主 goroutine(即 main)严格并行或按代码顺序执行。正如以下经典示例所示:
package main
import (
"fmt"
"time"
)
func main() {
go fmt.Println("Hello") // 启动 goroutine,但不阻塞当前流程
fmt.Println("World") // 主 goroutine 继续执行
time.Sleep(1 * time.Millisecond) // 为避免主 goroutine 提前退出,给子 goroutine 执行机会
}运行结果几乎总是:
World Hello
这常被误解为“goroutine 要等到 main 阻塞才开始执行”。实际上,goroutine 在 go 语句执行后即被调度器标记为就绪状态,可能随时被调度运行——但是否“立刻”运行,取决于当前调度器状态、底层线程(M)、处理器(P)资源以及是否存在抢占点。
Go 运行时会在以下典型位置插入调度点(yield points),允许调度器切换 goroutine:
- 函数调用(尤其是非内联的函数,如 fmt.Println 内部的 I/O 操作);
- 系统调用(如写入 stdout 触发的 write() 系统调用);
- select、channel 操作(含阻塞/非阻塞);
- runtime.Gosched() 显式让出;
- 长循环中编译器自动插入的抢占检查(Go 1.14+ 基于信号的异步抢占已大幅增强公平性)。
⚠️ 关键提醒:这些 yield points 是运行时内部实现细节,不构成语言规范保证。不同 Go 版本、不同平台(如 GOOS=js)、甚至不同编译优化级别都可能导致调度行为变化。因此:
- ❌ 绝不应编写依赖输出顺序的代码(例如假设 go f() 一定晚于后续语句执行);
- ✅ 必须使用同步原语显式协调 goroutine 时序,例如:
- sync.WaitGroup 等待 goroutine 完成;
- chan struct{} 或带缓冲 channel 传递完成信号;
- sync.Mutex / sync.RWMutex 保护共享状态;
- sync/atomic 进行无锁状态标记(如 atomic.Bool)。
正确示例(确保 “Hello” 先于 “World” 输出):
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Hello")
}()
wg.Wait() // 主 goroutine 等待子 goroutine 完成
fmt.Println("World")
}
// 输出确定为:
// Hello
// World总结:Goroutine 的“执行时间点”本质上是非确定性的(non-deterministic)。Go 的并发模型设计哲学是 “Don’t communicate by sharing memory; share memory by communicating” —— 你不需要、也不应该猜测何时执行,而应通过 channel、WaitGroup 等工具声明式地表达依赖关系。把调度交给 runtime,把正确性交给你自己写的同步逻辑。










