6.7 KiB
Gin Router and Explicit HTTP Server Composition Design
日期:2026-07-02
目标
将 codespace 后端的原生 Go 1.22 http.ServeMux 路由替换为 Gin,同时继续显式构造 http.Server。Gin 只负责路由和 handler 上下文,网络层仍由 http.Server 控制。
本次改动的核心要求:
- 使用 Gin 注册 API 路由。
- 不使用
router.Run()或engine.Run()。 - 将
*gin.Engine显式设置为http.Server.Handler。 - 将服务器网络参数配置化,包括 timeout 和最大请求头大小。
- 保持现有 API 路径、HTTP method、响应 JSON 结构不变。
当前状态
当前代码:
cmd/server/main.go已显式构造http.Server。internal/api/router.go使用http.NewServeMux()。- handler 使用
http.ResponseWriter、*http.Request、r.PathValue("id")和json.NewDecoder(r.Body)。 pkg/config只配置server.addr,timeout 仍硬编码在main.go。
非目标
本次不做:
- 不引入认证、CORS、rate limit、request logger middleware。
- 不改变 workspace/file/process service 接口。
- 不改变 API 路径、HTTP method、响应结构。
- 不实现 WebSocket、watcher 推送、Git、LSP、AI Agent。
- 不把网络层控制权交给 Gin 的
Run()。
目标架构
cmd/server/main.go
↓
config.Load()
↓
api.NewRouter(...)
↓
*gin.Engine
↓
http.Server{
Addr,
Handler: ginEngine,
ReadTimeout,
WriteTimeout,
IdleTimeout,
MaxHeaderBytes,
}
↓
ListenAndServe()
关键规则:
internal/api.NewRouter返回*gin.Engine。cmd/server/main.go显式创建http.Server。http.Server.Handler设置为 Gin engine。- 代码中不得出现
.Run(用于启动 Gin server。 ReadTimeout、WriteTimeout、IdleTimeout、MaxHeaderBytes来自配置。
配置设计
configs/config.yaml 扩展为:
server:
addr: ":8080"
readTimeout: "15s"
writeTimeout: "15s"
idleTimeout: "60s"
maxHeaderBytes: 1048576
workspace:
root: "./workspaces"
process:
opencodeCommand: "opencode"
pkg/config.ServerConfig 对外使用强类型:
type ServerConfig struct {
Addr string
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
MaxHeaderBytes int
}
YAML 加载不要依赖 time.Duration 的隐式解析。使用 raw config 字符串字段承接 YAML,然后通过 time.ParseDuration 显式解析。这样 "15s"、"1m"、"500ms" 都清晰可控。
环境变量增加:
CODESPACE_READ_TIMEOUTCODESPACE_WRITE_TIMEOUTCODESPACE_IDLE_TIMEOUTCODESPACE_MAX_HEADER_BYTES
已有环境变量保持不变:
CODESPACE_ADDRCODESPACE_WORKSPACE_ROOTCODESPACE_OPENCODE_COMMAND
默认值:
addr::8080readTimeout:15swriteTimeout:15sidleTimeout:60smaxHeaderBytes:1048576
路由设计
原路由保持不变:
GET /healthz
POST /api/workspaces
GET /api/workspaces/:id
DELETE /api/workspaces/:id
GET /api/workspaces/:id/files
GET /api/workspaces/:id/files/read
PUT /api/workspaces/:id/files/write
POST /api/workspaces/:id/files/mkdir
DELETE /api/workspaces/:id/files
POST /api/workspaces/:id/files/rename
POST /api/workspaces/:id/process/start
POST /api/workspaces/:id/process/stop
POST /api/workspaces/:id/process/restart
GET /api/workspaces/:id/process/status
Gin 注册方式:
r := gin.New()
r.GET("/healthz", healthHandler)
api := r.Group("/api")
api.POST("/workspaces", workspaceHandler.create)
api.GET("/workspaces/:id", workspaceHandler.get)
第一版使用 gin.New() 而不是 gin.Default(),避免隐式加入 logger middleware。只添加 gin.Recovery(),防止 panic 打爆进程。请求日志以后有真实需求再加。
Handler 迁移
签名从:
func (h *workspaceHandler) get(w http.ResponseWriter, r *http.Request)
改为:
func (h *workspaceHandler) get(c *gin.Context)
路径参数:
id := c.Param("id")
查询参数:
path := c.Query("path")
JSON body:
if err := c.ShouldBindJSON(&req); err != nil {
writeError(c, util.New(util.CodeBadRequest, "invalid json"))
return
}
响应:
c.JSON(http.StatusCreated, model.WorkspaceResponse{ID: ws.ID})
c.Status(http.StatusNoContent)
错误处理
保持现有错误 JSON:
{
"error": {
"code": "bad_request",
"message": "invalid json"
}
}
internal/api/errors_helper.go 改为接收 *gin.Context:
func writeError(c *gin.Context, err error)
继续使用 util.CodeOf(err) 映射:
bad_request-> 400not_found-> 404conflict-> 409- default -> 500
可以复用 pkg/response.ErrorBody 和 pkg/response.ErrorDetail,避免重复定义响应结构。
Server 组合
cmd/server/main.go 最终保持显式组合:
router := api.NewRouter(workspaceSvc, fileSvc, processSvc)
server := &http.Server{
Addr: cfg.Server.Addr,
Handler: router,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
IdleTimeout: cfg.Server.IdleTimeout,
MaxHeaderBytes: cfg.Server.MaxHeaderBytes,
}
启动仍然使用:
server.ListenAndServe()
测试策略
现有 httptest 测试继续有效,因为 *gin.Engine 实现 http.Handler。
需要验证:
GET /healthz行为不变。- workspace 创建、文件写读、process status 行为不变。
- Gin path param
:id正常传入 service。 - 配置默认值正确。
- YAML timeout 字符串能解析。
- 环境变量能覆盖 timeout 和
maxHeaderBytes。 go test ./...通过。grep -R "\.Run(" cmd internal || true无输出。go run ./cmd/server后/healthz返回{"status":"ok"}。
依赖
新增唯一依赖:
github.com/gin-gonic/gin
不新增其他框架或中间件依赖。
风险与处理
- 风险:Gin 默认模式可能输出 debug 信息。处理:启动时可设置
gin.SetMode(gin.ReleaseMode),或保留 Gin 默认但不影响功能。第一版建议在NewRouter中设置gin.ReleaseMode,减少噪音。 - 风险:
time.DurationYAML 解析行为不符合预期。处理:使用 raw string +time.ParseDuration。 - 风险:错误响应格式变化。处理:复用现有响应结构并保留 HTTP 测试。
- 风险:误用
router.Run()。处理:增加 grep 验证。
完成标准
完成后必须满足:
internal/api使用 Gin 注册路由。cmd/server/main.go使用http.Server{Handler: router}。- 不出现 Gin
Run()启动服务器。 - server 网络参数来自配置。
- API 行为和响应结构保持兼容。
go test ./...通过。- server smoke test 通过。