
time.Ticker 和 time.AfterFunc 不是调度器,只是定时信号发生器;Golang 真正的调度器(GMP 模型)无法“手动调节”任务顺序或优先级——它只管 goroutine 在 M 个 OS 线程上怎么跑,不管业务逻辑该谁先执行。你要的「任务调度」,得自己实现。
用 container/heap 做优先级队列,别手写排序
标准库没现成优先级队列,但 container/heap 是唯一轻量可控的选择。自己用 sort.Slice 或切片插入再重排,每次增删都要 O(n log n),且无法动态调整执行时间。
-
Less(i, j int) bool必须先比Priority(数值越小越高),再比CreatedAt或ExecTime,否则同优先级任务会饿死 - 每次
heap.Push或heap.Pop后,必须调用heap.Fix(pq, i)或heap.Init(pq),否则堆结构失效,下次Pop可能返回错的任务 - 别在
Less里查 DB、加锁、调 API——它被高频调用,阻塞会卡住整个调度循环
time.Timer 配合队列做动态重调度,不是简单 Reset
想插队、取消、延迟重试?不能靠反复 time.AfterFunc 递归,也不能裸用 time.NewTimer 后乱 Reset。正确做法是:全局一个 *time.Timer,始终指向队列中最近要执行的任务。
-
Timer.Reset前必须先if !t.Stop() { },否则旧 timer 仍在后台发信号,可能 panic:send on closed channel或invalid memory address - 任务入队/出队/优先级变更后,立刻重新计算堆顶的
ExecTime,然后Reset全局 timer;如果堆空了,就不用Reset,也不用Stop(Stop 已安全) - timer 触发时,不要直接执行业务函数——用 goroutine 异步跑,避免阻塞调度主循环;同时检查当前任务是否已被取消(比如
task.Status == Canceled)
worker pool 必须带 recover + context + 限流
每个任务裸起 goroutine,一次 panic 就崩掉整个进程;不设超时,一个慢任务就能拖垮全部 worker。
- worker 数量别硬写死,I/O 密集型建议设为
runtime.NumCPU() * 3,CPU 密集型就用runtime.NumCPU() - 每个 worker 内部必须包一层
defer func() { recover() }(),否则任意 handler panic 都会让 worker 退出,队列积压 - 任务执行前用
ctx, cancel := context.WithTimeout(parentCtx, task.Timeout),执行完立刻cancel();别依赖 HTTP 层 timeout,它管不到你自己的 goroutine - 用
golang.org/x/sync/semaphore控制并发上限,比无缓冲 channel 更易观测和调试
别碰 runtime.GOMAXPROCS,除非你在容器里跑满核
默认值就是最优解——等于 CPU 核心数。强行设小,goroutine 排队;设大,线程切换开销反升。真正影响吞吐的是你的任务结构和 worker 设计,不是 P 的数量。
-
GOMAXPROCS调整只对 CPU 密集型程序有意义,而绝大多数后台任务是 I/O 密集型 - 容器环境(如 Kubernetes)下若限制了 CPU limit,
runtime.NumCPU()可能读错,此时应读/sys/fs/cgroup/cpu/cpu.cfs_quota_us和/sys/fs/cgroup/cpu/cpu.cfs_period_us手动算可用核数 - 改了
GOMAXPROCS后不重启服务,新设置不会生效;但 runtime 包本身不提供 reload 接口,别在运行时反复调runtime.GOMAXPROCS()
复杂点不在堆怎么建,而在状态怎么管:任务失败后要不要进重试队列?重试次数耗尽后要不要回调告警?所有这些,都得在 Handler 执行完之后、worker 循环继续前,同步更新到持久化存储或消息队列里——否则进程一挂,任务就丢了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











