You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go服务无法即时响应GET请求,请求被超时检查阻塞需修复

问题分析

你遇到的核心问题是HTTP请求处理goroutine被阻塞了。在createGame函数里,你直接调用了checkTimeout(id),而checkTimeout里的<-game.Timer会一直等待定时器触发(也就是20秒后)才会继续执行,这就导致处理/create_game/{name}请求的goroutine被卡住,没法即时给客户端返回响应。

修复方案

要解决这个问题,关键是把超时检查的逻辑放到独立的goroutine中执行,让HTTP请求处理的goroutine能立刻返回响应。同时还要处理并发安全的问题——因为多个goroutine(HTTP请求、超时处理、WebSocket广播)会同时操作games切片,需要用互斥锁保护它。

具体修改步骤

  1. 启动独立goroutine执行超时检查:在createGame里调用checkTimeout时,加上go关键字,让它在后台运行,不阻塞当前请求。
  2. 添加互斥锁保护共享资源:给games、genID这些共享变量添加sync.Mutex,避免并发读写导致的竞态条件。
  3. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:19:44