
在 Go 应用中,可通过 go 关键字将后台任务(如 Redis 监听、定时检查)并发启动,与 http.ListenAndServe 共存;但必须避免空循环、确保资源清理,并合理使用 channel 或 select{} 防止主线程退出。
在 go 应用中,可通过 go 关键字将后台任务(如 redis 监听、定时检查)并发启动,与 http.listenandserve 共存;但必须避免空循环、确保资源清理,并合理使用 channel 或 select{} 防止主线程退出。
Go 的并发模型天然适合“服务监听 + 后台任务”这类混合场景:HTTP 服务器本身由 net/http 自动为每个请求分配 goroutine,而长期运行的后台任务(如 Redis Pub/Sub 监听、缓存刷新、健康检查)则应显式启动独立 goroutine。但简单地 go checkExpire() 并不足够——关键在于结构化、可终止、资源安全。
✅ 正确做法:带生命周期管理的后台 goroutine
以下是一个生产就绪的模板,整合了 HTTP 服务与 Redis Pub/Sub 监听:
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"github.com/go-redis/redis/v8"
)
var ctx = context.Background()
var rdb *redis.Client
func initRedis() {
rdb = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
if _, err := rdb.Ping(ctx).Result(); err != nil {
log.Fatal("failed to connect to Redis:", err)
}
}
// 后台任务:监听 Redis 频道,解耦接收与处理
func listenRedisNotifications() {
pubsub := rdb.Subscribe(ctx, "notifications")
defer pubsub.Close() // 确保连接释放
// 使用缓冲 channel 解耦消息接收与业务处理
msgCh := make(chan string, 100)
// goroutine 1:持续接收消息(阻塞式)
go func() {
for {
msg, err := pubsub.ReceiveMessage(ctx)
if err != nil {
log.Printf("Redis receive error: %v", err)
return // 连接断开时退出
}
select {
case msgCh <h3>⚠️ 关键注意事项</h3>
-
禁止空循环
for {}:它会 100% 占用一个 CPU 核心,且无法响应信号或优雅退出。永远用time.Sleep、channel、select{}或上下文控制等待。 -
Pub/Sub 必须专用 client:不要与普通命令(
GET/SET)共用同一redis.Client,因其连接池语义冲突;应单独配置PoolSize: 1并禁用 idle 检查:&redis.Options{ Addr: "localhost:6379", PoolSize: 1, // 固定单连接 IdleCheckFrequency: -1, // 禁用心跳驱逐 } -
始终
defer Close():无论是*http.Client、*redis.Client还是*redis.PubSub,未关闭会导致连接泄漏,Redis 侧堆积 SUBSCRIBE 连接。 -
主线程不能提前退出:
http.ListenAndServe是阻塞调用,它会维持主 goroutine 运行;若你改用非阻塞方式(如http.Serve),则需配合sync.WaitGroup或select{}阻塞主 goroutine,否则程序立即退出。
? 总结
启动后台任务不是“加个 go 就完事”,而是要遵循三原则:
? 分离关注点:接收(I/O)、分发(channel)、处理(业务)三者解耦;
? 可控生命周期:通过 context.Context 或 channel 信号支持优雅停止(本文示例可扩展添加 shutdown logic);
? 资源确定性:所有连接、订阅、goroutine 都有明确的创建与销毁路径。
这样构建的服务,既保持高并发吞吐,又具备可观测性与稳定性,真正发挥 Go “Concurrency is not Parallelism” 的设计哲学。










