# 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()`。 ## 目标架构 ```text cmd/server/main.go ↓ config.Load() ↓ api.NewRouter(...) ↓ *gin.Engine ↓ http.Server{ Addr, Handler: ginEngine, ReadTimeout, WriteTimeout, IdleTimeout, MaxHeaderBytes, } ↓ ListenAndServe() ``` 关键规则: 1. `internal/api.NewRouter` 返回 `*gin.Engine`。 2. `cmd/server/main.go` 显式创建 `http.Server`。 3. `http.Server.Handler` 设置为 Gin engine。 4. 代码中不得出现 `.Run(` 用于启动 Gin server。 5. `ReadTimeout`、`WriteTimeout`、`IdleTimeout`、`MaxHeaderBytes` 来自配置。 ## 配置设计 `configs/config.yaml` 扩展为: ```yaml server: addr: ":8080" readTimeout: "15s" writeTimeout: "15s" idleTimeout: "60s" maxHeaderBytes: 1048576 workspace: root: "./workspaces" process: opencodeCommand: "opencode" ``` `pkg/config.ServerConfig` 对外使用强类型: ```go 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_TIMEOUT` - `CODESPACE_WRITE_TIMEOUT` - `CODESPACE_IDLE_TIMEOUT` - `CODESPACE_MAX_HEADER_BYTES` 已有环境变量保持不变: - `CODESPACE_ADDR` - `CODESPACE_WORKSPACE_ROOT` - `CODESPACE_OPENCODE_COMMAND` 默认值: - `addr`: `:8080` - `readTimeout`: `15s` - `writeTimeout`: `15s` - `idleTimeout`: `60s` - `maxHeaderBytes`: `1048576` ## 路由设计 原路由保持不变: ```text 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 注册方式: ```go 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 迁移 签名从: ```go func (h *workspaceHandler) get(w http.ResponseWriter, r *http.Request) ``` 改为: ```go func (h *workspaceHandler) get(c *gin.Context) ``` 路径参数: ```go id := c.Param("id") ``` 查询参数: ```go path := c.Query("path") ``` JSON body: ```go if err := c.ShouldBindJSON(&req); err != nil { writeError(c, util.New(util.CodeBadRequest, "invalid json")) return } ``` 响应: ```go c.JSON(http.StatusCreated, model.WorkspaceResponse{ID: ws.ID}) c.Status(http.StatusNoContent) ``` ## 错误处理 保持现有错误 JSON: ```json { "error": { "code": "bad_request", "message": "invalid json" } } ``` `internal/api/errors_helper.go` 改为接收 `*gin.Context`: ```go func writeError(c *gin.Context, err error) ``` 继续使用 `util.CodeOf(err)` 映射: - `bad_request` -> 400 - `not_found` -> 404 - `conflict` -> 409 - default -> 500 可以复用 `pkg/response.ErrorBody` 和 `pkg/response.ErrorDetail`,避免重复定义响应结构。 ## Server 组合 `cmd/server/main.go` 最终保持显式组合: ```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, } ``` 启动仍然使用: ```go 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"}`。 ## 依赖 新增唯一依赖: ```text github.com/gin-gonic/gin ``` 不新增其他框架或中间件依赖。 ## 风险与处理 - 风险:Gin 默认模式可能输出 debug 信息。处理:启动时可设置 `gin.SetMode(gin.ReleaseMode)`,或保留 Gin 默认但不影响功能。第一版建议在 `NewRouter` 中设置 `gin.ReleaseMode`,减少噪音。 - 风险:`time.Duration` YAML 解析行为不符合预期。处理:使用 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 通过。