goroutine不是线程,而是go运行时调度的轻量级协程,初始栈仅2kb、用户态切换、m:n调度;runtime.gomaxprocs控制p(逻辑处理器)数量,默认等于cpu核心数,决定并行执行能力上限。

goroutine 不是多线程,它只是 Go 运行时调度的轻量级执行单元。你写 go f(),启动的不是操作系统线程,而是一个可能复用底层 OS 线程(M)的协程。真正决定并发能力上限的,是 Go 调度器如何把 G(goroutine)、M(OS thread)、P(processor,逻辑处理器)三者动态绑定。
为什么 go 启动的不是线程?
操作系统线程(如 pthread)创建成本高、栈固定(Linux 默认 2MB),而 goroutine 初始栈仅 2KB,按需增长;上下文切换在用户态完成,不触发系统调用。这意味着:
- 启动 10 万个 goroutine 内存开销约 200MB,而同等数量的 OS 线程会直接 OOM
- goroutine 阻塞(如读网络、time.Sleep、channel 操作)时,Go 调度器会自动将其从当前 M 上剥离,让出线程给其他就绪的 G
- 你无法控制哪个 G 在哪个 M 上跑,调度完全由 runtime.scheduler 决定
runtime.GOMAXPROCS 控制什么?
它设置的是可同时运行的 P 数量,即“逻辑处理器”个数,默认等于 CPU 核心数(runtime.NumCPU())。注意:
- P 是调度器的资源池,每个 P 维护一个本地 runqueue,存放待执行的 G
- M 必须绑定到一个 P 才能执行 G;当 M 因系统调用阻塞时,会尝试将绑定的 P 转交给其他空闲 M
- 设置 runtime.GOMAXPROCS(1) 并不会禁止并发,只是强制所有 G 串行争抢同一个 P,此时仍可能因 I/O 或 channel 阻塞发生协作式让出
- 调整它通常只在特殊场景有用(如调试竞态、限制资源),生产环境一般保持默认
常见陷阱:main 退出导致 goroutine 被丢弃
这是新手最常踩的坑——main 函数返回,整个进程立即终止,所有未结束的 goroutine 被强制杀死,且不保证任何清理逻辑执行。
- 错误写法:go doWork(); fmt.Println("done") → doWork 很可能根本没机会执行完
- 正确做法取决于场景:
- 等待结果:用 channel + 接收,或 <code>sync.WaitGroup 计数
- 后台长期服务:用 select{} 阻塞住 main,配合信号监听或 http.Server 的 Shutdown
- 定时任务:用 time.Ticker + context.WithCancel 控制生命周期
- 特别注意:匿名函数捕获循环变量时,若不显式传参(如 go f(i)),所有协程会共享最终的 i 值(闭包陷阱)
什么时候真需要 OS 线程?
绝大多数 Go 场景不需要手动管理线程。只有两类例外:
- 调用 C 代码且该 C 函数会阻塞 OS 线程(如某些数据库驱动、硬件 SDK),需用 runtime.LockOSThread() 绑定,防止 M 被偷走
- 极少数需要独占 CPU 核心做实时计算(如高频交易策略),可通过 syscall.SchedSetaffinity 绑核,但这已脱离 Go 原生并发模型,属于混合编程范畴
- 即便如此,也应优先考虑用 chan + select + context 构建响应式流水线,而非裸线程同步
真正难的不是启动协程,而是理解它何时被调度、何时被阻塞、何时被抢占,以及如何用 channel 和 context 描述它的生命周期边界。这些边界一旦模糊,就会出现 goroutine 泄漏、数据竞争或静默失败——而它们往往在压测或上线后才暴露。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











