runtime.newproc是go启动goroutine的唯一入口,它不直接执行函数,仅负责准备和入队;因普通goroutine栈仅2kb,不足以完成g结构体分配、调度初始化等操作,故必须通过systemstack切换至g0的大栈(64kb+)执行newproc1,再由runqput将新g置为_grunnable状态并入队等待调度。

runtime.newproc 是 Go 启动 goroutine 的唯一入口,它不直接执行函数,只负责准备和入队 —— 你写的 go f() 最终都会走到这里。
为什么 newproc 必须切换到系统栈执行
普通 goroutine 的栈很小(默认 2KB),而创建新 goroutine 需要分配结构体、初始化调度信息、操作全局链表等,这些操作不能在受限栈上做。所以 runtime.newproc 用 systemstack 切到 g0 的大栈(通常 64KB+)再调用 newproc1。
- 如果当前 goroutine 就是
g0或gsignal,systemstack直接执行传入函数 - 否则先保存当前栈现场,切到
g0栈,执行完再切回来 - 这个切换不是用户可控的,也别试图绕过 —— 一旦在小栈里 malloc 或遍历
allgs,大概率栈溢出 panic
newproc1 如何获取和初始化 g 对象
newproc1 是实际干活的函数,它从本地空闲池 _p_.gfree 拿 g,拿不到才新建;初始化后状态设为 _Grunnable,但此时还不会运行。
- 优先从 P 的本地
gfree链表取,快且无锁 - 本地没有就从全局
sched.gfree取,需加锁 - 全都没了才调
malg(_StackMin)分配新栈(最小 2KB),并把g加入allgs全局列表 -
g.sched.pc设为goexit+PCQuantum,确保退出时能正确清理栈和资源 -
gostartcallfn把用户函数地址和参数塞进g.sched,后续由 M 执行时跳转
goroutine 入队逻辑:runqput 与 runnext 的区别
新 g 创建完后,调用 runqput 放入可运行队列 —— 但放哪、怎么放,直接影响调度延迟。
- 默认放进当前 P 的本地队列尾部(
runqput(_p_, g, false)) - 如果
next == true(比如某些 runtime 内部 fast-path 场景),会塞进_p_.runnext,下一次调度优先取它,相当于“插队” - 本地队列满(长度 256)时,自动 fallback 到全局队列
sched.runq,这时需加锁且有竞争 - 注意:
runqput不唤醒 M —— 唤醒由后续的wakep触发,前提是已有空闲 P 且没活跃 M
刚创建的 goroutine 为何不立即执行
从 runtime.newproc 返回后,你的 go f() 就结束了,但 f 还没跑。它只是被标记为 _Grunnable 并入队,是否立刻执行取决于调度器状态。
- 没有空闲 P:新
g在队列里等着,直到某个 M 归还 P 或新 M 被创建 - 有空闲 P 但没空闲 M:触发
wakep,尝试唤醒休眠 M;失败则新建 M(受sched.maxmcount限制) - 即使一切就绪,也要等当前 M 执行完当前
g的时间片(非抢占式,除非发生 syscall 或 GC STW) - 所以不要假设
go f()后f立刻开始 —— 它只是“已提交”,不是“已启动”
真正容易被忽略的是:整个流程里没有一处涉及操作系统线程创建(M)或 CPU 核心绑定(P),全是 runtime 自己维护的抽象。你看到的“并发”,其实是 G 在有限几个 M 上按需切换,而 P 控制着最大并行度 —— 这个解耦才是 Go 调度器轻量的关键。写业务代码时,别去碰 g 或 newproc 的细节,但理解它不立即执行、不保证顺序、依赖 P/M 状态,能帮你避开很多竞态和延迟误判。











