
在 Go 应用中,可通过 go 启动协程实现 HTTP 服务监听与后台任务(如 Redis 订阅)的并发执行;关键在于避免空循环、合理使用阻塞机制,并确保资源安全释放。
在 go 应用中,可通过 `go` 启动协程实现 http 服务监听与后台任务(如 redis 订阅)的并发执行;关键在于避免空循环、合理使用阻塞机制,并确保资源安全释放。
在构建现代 Go 后台服务时,常需同时运行 HTTP 服务器与长期运行的后台任务(例如监听 Redis Pub/Sub、轮询外部 API、清理过期缓存等)。初学者易陷入两个误区:一是将后台逻辑直接写在 main() 函数末尾导致阻塞服务启动;二是使用无休止的空循环 for {} 消耗 CPU。正确做法是将后台任务封装为独立函数,并通过 go 关键字异步启动——这正是 Go 并发模型的核心优势。
以下是一个结构清晰、生产可用的示例,融合了 HTTP 服务与 Redis 订阅任务:
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.Fatalf("Failed to connect to Redis: %v", err)
}
}
func listenRedisNotifications() {
pubsub := rdb.Subscribe(ctx, "notifications")
defer pubsub.Close() // 确保连接释放
log.Println("Subscribed to Redis channel 'notifications'")
for {
msg, err := pubsub.ReceiveMessage(ctx)
if err != nil {
log.Printf("Redis subscription error: %v", err)
return // 或重连逻辑
}
log.Printf("Received notification: %s → %s", msg.Channel, msg.Payload)
// ✅ 在此处处理业务逻辑(如更新缓存、触发事件)
// 注意:若处理较重,建议投递到缓冲 channel 或 worker pool,避免阻塞接收
}
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Go server! Time: %s", time.Now().UTC().Format(time.RFC3339))
}
func main() {
initRedis()
// ✅ 启动后台任务:监听 Redis,不阻塞主流程
go listenRedisNotifications()
// ✅ 注册 HTTP 路由
http.HandleFunc("/", handler)
// ✅ 启动 HTTP 服务(此调用会阻塞,因此必须放在最后)
log.Println("HTTP server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}关键要点说明:
-
go启动协程是标准且推荐的做法:net/http本身已为每个请求分配 goroutine,而listenRedisNotifications()这类长生命周期任务天然适合独立 goroutine。 -
禁止空循环
for {}:它会持续占用一个 CPU 核心,造成严重资源浪费。应始终配合time.Sleep、channel接收、context.Done()或pubsub.ReceiveMessage()等阻塞/等待原语。 -
资源清理不可忽视:
defer pubsub.Close()确保订阅连接在 goroutine 退出时被释放,防止 Redis 侧连接泄漏(SUBSCRIBE连接不归还连接池,需显式关闭)。 -
错误处理要健壮:
ReceiveMessage()返回错误时(如网络中断),应考虑重连机制或优雅降级,而非直接 panic。 -
避免混用连接池:如需高可靠订阅,建议为 Pub/Sub 单独创建 client(
PoolSize: 1+IdleCheckFrequency: -1),避免与常规GET/SET命令争抢连接池资源(见 Redis 官方最佳实践)。
最后提醒:main() 函数本身就是一个 goroutine,但它一旦返回,整个程序即终止。因此,http.ListenAndServe 必须作为最后一条语句(或通过 sync.WaitGroup 等方式协调退出),确保主 goroutine 不提前结束——这是并发程序生命周期管理的基础原则。

















