Go服务无法即时响应GET请求,请求被超时检查阻塞需修复
问题分析
你遇到的核心问题是HTTP请求处理goroutine被阻塞了。在createGame函数里,你直接调用了checkTimeout(id),而checkTimeout里的<-game.Timer会一直等待定时器触发(也就是20秒后)才会继续执行,这就导致处理/create_game/{name}请求的goroutine被卡住,没法即时给客户端返回响应。
修复方案
要解决这个问题,关键是把超时检查的逻辑放到独立的goroutine中执行,让HTTP请求处理的goroutine能立刻返回响应。同时还要处理并发安全的问题——因为多个goroutine(HTTP请求、超时处理、WebSocket广播)会同时操作games切片,需要用互斥锁保护它。
具体修改步骤
- 启动独立goroutine执行超时检查:在
createGame里调用checkTimeout时,加上go关键字,让它在后台运行,不阻塞当前请求。 - 添加互斥锁保护共享资源:给
games、genID这些共享变量添加sync.Mutex,避免并发读写导致的竞态条件。 - 修改Game结构体存储Timer指针:原来的
Timer <-chan time.Time没法停止定时器,改成*time.Timer,方便后续在需要时停止定时器,避免资源泄漏。
修改后的完整代码
import ( "encoding/json" "fmt" "log" "net/http" "sync" "time" "github.com/gorilla/mux" "github.com/gorilla/websocket" ) var clients = make(map[*websocket.Conn]bool) var broadcast = make(chan Game) // 新增互斥锁,保护共享资源 var mu sync.Mutex // GAME_TIMEOUT in seconds const GAME_TIMEOUT = 20 // ID generating var genID = 0 var games = []Game{} var msg json.RawMessage var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }, } type GameToSend struct { Type string `json:"type"` ID int `json:"id"` Name string `json:"name"` } // 修改Game结构体,存储*time.Timer而非只读通道 type Game struct { ID int `json:"id"` Name string `json:"name"` Timer *time.Timer `json:"-"` // json:"-" 避免序列化定时器 } func main() { router := mux.NewRouter() router.HandleFunc("/create_game/{name}", createGame) router.HandleFunc("/game_events", handleConnections) http.ListenAndServe(":8080", router) } func handleConnections(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Fatal(err) } defer conn.Close() mu.Lock() clients[conn] = true // 发送现有游戏列表时加锁 for _, game := range games { conn.WriteJSON(GameToSend{"game.created", game.ID, game.Name}) } mu.Unlock() for { err := conn.ReadJSON(&msg) if err != nil { mu.Lock() delete(clients, conn) mu.Unlock() fmt.Println(err) break } else { fmt.Println(msg) } } } func broadcastGame(game GameToSend) { mu.Lock() defer mu.Unlock() for conn := range clients { err := conn.WriteJSON(game) if err != nil { log.Printf("broadcast error: %v", err) conn.Close() delete(clients, conn) } } } func createGame(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) mu.Lock() genID++ timer := time.NewTimer(GAME_TIMEOUT * time.Second) game := Game{genID, params["name"], timer} games = append(games, game) // 广播前先解锁,避免阻塞 gameToSend := GameToSend{"game.created", game.ID, game.Name} mu.Unlock() broadcastGame(gameToSend) // 启动独立goroutine执行超时检查,不阻塞当前HTTP请求 go checkTimeout(genID) w.Write([]byte("response")) } func deleteGame(actionType string, id int) { mu.Lock() defer mu.Unlock() for index, game := range games { if game.ID == id { // 停止定时器,避免资源泄漏 game.Timer.Stop() broadcastGame(GameToSend{actionType, game.ID, game.Name}) // 正确删除切片元素 games = append(games[:index], games[index+1:]...) break } } } func checkTimeout(id int) { // 先获取对应的定时器 mu.Lock() var timer *time.Timer for _, game := range games { if game.ID == id { timer = game.Timer break } } mu.Unlock() if timer == nil { return // 游戏已经被删除,直接返回 } <-timer.C deleteGame("game.timeout", id) }
关键修改点说明
go checkTimeout(genID):把超时检查放到后台goroutine,让createGame能立刻返回HTTP响应。sync.Mutex:所有对games、genID、clients的读写操作都加锁,避免并发访问导致的数据竞争。- Game结构体的Timer字段:改成
*time.Timer,并添加json:"-"避免序列化,同时在删除游戏时调用Stop()防止定时器资源泄漏。 - 切片删除逻辑优化:原来的
games = games[:index+copy(games[index:], games[index+1:])]可以简化为games = append(games[:index], games[index+1:]...),更易读。
内容的提问来源于stack exchange,提问作者Davor Buha
相关产品推荐
相关产品推荐

