go 程序默认使用全部逻辑 cpu(go 1.5+),但受 gomaxprocs 设置、容器 cgroup 限制、阻塞操作、锁竞争及 io 模式影响,常无法跑满多核;需检查配置、避免阻塞、用原子操作替代锁、合理复用连接,并聚焦真正 cpu 密集型任务做 worker pool 分发。

Go 程序默认不跑满多核?检查 GOMAXPROCS 设置
Go 运行时默认将 GOMAXPROCS 设为系统逻辑 CPU 数(Go 1.5+),但若程序启动前被显式设为 1,或在容器中未正确识别 CPU 数量(如 docker run --cpus=2 但未透传 /sys/fs/cgroup/cpu/cpu.cfs_quota_us),就会退化为单核调度。用 runtime.GOMAXPROCS(0) 可读取当前值,建议在 main 开头打印确认:
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
常见陷阱:
- 在
init()中调用runtime.GOMAXPROCS(1)后忘记恢复 - Kubernetes Pod 设置了
resources.limits.cpu: "1",但 Go 1.19+ 才自动适配 cgroup v2 的 CPU quota - 交叉编译后在低核数设备运行,未重新校准
阻塞型 goroutine 会卡住 P,导致核心空转
真正拖慢多核利用率的往往不是 CPU 密集型任务,而是看似“轻量”的阻塞操作:比如未设超时的 http.Get、无缓冲 channel 的发送、time.Sleep 长等待、或同步原语(sync.Mutex)争抢激烈。这些会让 goroutine 挂起,但其绑定的 P(Processor)无法被其他 goroutine 复用。
排查方法:
立即学习“go语言免费学习笔记(深入)”;
- 用
go tool trace查看“Scheduling Latency”和“Syscall”事件分布 - 开启
GODEBUG=schedtrace=1000观察 P 是否长期处于idle或syscall状态 - 对 HTTP 客户端强制设置
Timeout和Transport.IdleConnTimeout
避免共享内存竞争:用 sync/atomic 替代 sync.Mutex 计数
高频更新的计数器(如请求总量、错误数)若用 sync.Mutex 保护,会在多核间引发 cache line bouncing,显著降低吞吐。例如:
var mu sync.Mutex<br>var total int<br>// 每次都要锁 → 热点
改用 atomic.Int64(Go 1.19+)或 atomic.AddInt64:
var total atomic.Int64<br>total.Add(1) // 无锁,直接 CPU 原子指令
适用场景有限制:
- 仅支持整数、指针类型的基本操作(加减、比较交换、载入存储)
- 不能替代复杂临界区逻辑(如“先查再删”需用
sync.RWMutex或更高级并发结构) - 注意
atomic.Value写入成本高于读取,勿频繁写大对象
IO 密集型任务别盲目开 goroutine,用 net/http.Server 自带的连接池
新手常以为“开 1000 个 goroutine 就能压满 10 核”,但实际网络 IO 不是 CPU 绑定的。HTTP Server 默认已启用多路复用和连接复用,手动为每个请求启 goroutine 反而增加调度开销。关键优化点是:
- 禁用
http.DefaultTransport的全局连接池滥用(它会复用连接但可能跨服务污染) - 为不同下游服务创建独立
http.Transport,并调高MaxIdleConnsPerHost(如设为 100) - 用
context.WithTimeout控制单请求生命周期,防止 goroutine 泄漏
真正需要横向扩展 CPU 利用率的,是 CPU 密集型子任务(如 JSON 解析、图像缩放、加密计算),这类才适合切分后丢进 worker pool 模式,由固定数量 goroutine(通常 ≈ GOMAXPROCS)轮询处理。
最易被忽略的一点:CPU 利用率高 ≠ 性能好。如果 pprof cpu profile 显示大量时间花在 runtime.futex 或 runtime.usleep,说明不是算力瓶颈,而是调度或 IO 卡点——这时候压核反而加剧争抢。










