Revel Controller中直接go func()易致goroutine泄漏,因每次请求新建Controller实例,但未受控goroutine若无超时、取消或channel关闭机制,会卡在无缓冲chan写入、time.AfterFunc未绑定context、db.QueryRowContext用background等场景。

Controller里直接go func()为什么容易泄漏
Revel每次请求都会新建一个*revel.Controller实例,但很多人误以为“用完就丢”,结果在Action方法里写go func() { ... }(),又不加超时、取消或等待机制,导致goroutine长期卡住。常见卡点包括:
• 无缓冲chan写入未关闭,goroutine停在ch <br>• 用了<code>time.AfterFunc或time.Tick但没绑定request.Context().Done(),timer持有controller引用不让GC
• 数据库查询传了context.Background(),连接迟迟不归还,底层goroutine被池子拖住
• 中间件中异步处理响应体(如审计日志),但没监听c.Request.Context().Done(),客户端断连后goroutine还在等IO
异步任务该用AsyncTask还是独立Worker
看任务性质:
• 短生命周期、强请求上下文依赖(比如上报本次请求的埋点、发个确认邮件)→ 用AsyncTask封装channel+recover,主线程Wait(timeout)防阻塞
• 长周期、需失败重试、多实例共享、状态追踪 → 别碰goroutine,老实用Redis+go-redis建独立worker进程
• AsyncTask示例里done和panic channel都带缓冲(make(chan error, 1)),否则主线程select可能永远等不到;但别设太大,1就够了,避免堆积
• 独立worker必须用defer/recover包住单个任务,失败推入queue:dlq,不能让panic干掉整个进程
定时任务用time.Ticker还是系统cron
选哪个取决于可靠性要求:
• 单实例、非关键任务(缓存预热、日志清理)→ time.Ticker够用,但只在app.Init()里启动一次,加if revel.BUILD_MODE == "prod"防开发模式重复创建
• 多实例、强一致性(每天只发一次报表)、部署解耦 → 必须用cron调cmd/scheduler/main.go,且路径全用绝对路径,crontab里显式声明PATH
• time.Ticker goroutine里别用revel.AppLog,初始化顺序可能导致输出为空,改用log.Print
• systemd管理时,Restart=on-failure比always安全,后者可能拉起两个ticker并发跑
pprof查goroutine泄漏的关键动作
生产环境别靠猜,直接抓现场:
• 确保app.conf里没注释app.debug = true,且/debug/pprof路由没被拦截
• 抓堆栈:wget http://localhost:9000/debug/pprof/goroutine?debug=2 -O goroutines.txt,搜running状态里反复出现的函数名
• 查阻塞:go tool pprof http://localhost:9000/debug/pprof/block,重点关注sync.runtime_SemacquireMutex下游调用链
• 如果goroutine数每秒+1,结合access log定位触发URL,复现时加runtime.Stack打快照
• 最容易被忽略的是:App.Init里new的全局对象(比如*redis.Client)若偷偷缓存了c.RenderArgs,会把整批controller钉在内存里


















