This repository has been archived on 2026-07-17. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
codespace/docs/superpowers/specs/2026-07-02-gin-server-composition-design.md
T
2026-07-02 15:07:54 +08:00

6.7 KiB
Raw Blame History

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.Requestr.PathValue("id")json.NewDecoder(r.Body)
  • pkg/config 只配置 server.addrtimeout 仍硬编码在 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()

关键规则:

  1. internal/api.NewRouter 返回 *gin.Engine
  2. cmd/server/main.go 显式创建 http.Server
  3. http.Server.Handler 设置为 Gin engine。
  4. 代码中不得出现 .Run( 用于启动 Gin server。
  5. ReadTimeoutWriteTimeoutIdleTimeoutMaxHeaderBytes 来自配置。

配置设计

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_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

路由设计

原路由保持不变:

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 -> 400
  • not_found -> 404
  • conflict -> 409
  • default -> 500

可以复用 pkg/response.ErrorBodypkg/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.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 通过。