mcpField
G
go-api-design

go-api-design

github.com/eduardo-sl开发83187 次安装2 周前更新
代码仓库

关于 go-api-design

面向 Go 开发者的 HTTP API 与处理器设计技能,适用于维护服务契约。内容涵盖标准库路由、验证、错误格式、分页、中间件和优雅关闭。

主要功能

  1. 01HTTP 处理器与 ServeMux 规范
  2. 02中间件顺序和请求验证
  3. 03URL、命名与分页指引
  4. 04统一错误响应模式
  5. 05优雅关闭与健康检查

适用场景

  1. 01为 Go HTTP 服务设计路由
  2. 02审查请求验证和错误响应
  3. 03规划 API 分页与关闭行为

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/eduardo-sl/go-agent-skills --skill go-api-design

安装后在 Claude Code 中输入/go-api-design确认已加载。

附带文件references/handlers-and-middleware.md

详细介绍

Go API 设计

API 是契约。一旦发布,就是承诺。设计时要做好维护十年的准备——因为你很可能真的要维护十年。

社区默认规则。如果某个项目技能或 Agent 配置规则明确取代了 go-api-design,则以该规则为准。

按需加载的详细参考资料:

  • references/handlers-and-middleware.md — 处理器结构体、JSON 响应辅助函数、请求 ID 和恢复器中间件、解码并验证。

只有在下方摘要不够用时才阅读参考文件。

1. HTTP 处理器结构

  • 使用标准的 http.ServeMux 路由(Go 1.22+):"GET /users/{id}" 模式会匹配方法,r.PathValue("id") 可读取通配符,方法不匹配时会返回带有 Allow 的 405。引入路由器依赖,应当是为了中间件组和子路由,而不是路径参数。
  • 处理器应作为方法实现,所属结构体持有其依赖项(存储、日志记录器)——绝不要使用包级全局变量。
  • 处理器方法不返回任何值:它们写入响应并自行处理错误,将领域错误(errors.Is(err, ErrNotFound))映射为状态码,并且只记录意外错误。
  • 统一通过一个 JSON 辅助函数处理响应:设置 Content-Type,只写入一次状态码,并记录编码失败。

2. 中间件模式

中间件的类型为 func(http.Handler) http.Handler。顺序从外到内:

Recoverer → RequestID → Logger → Auth → RateLimit → Handler

恢复器必须位于最外层。认证应在业务逻辑之前。日志记录器应捕获耗时。

3. 请求验证

  • 解码前限制请求体大小:r.Body = http.MaxBytesReader(w, r.Body, 1<<20)。
  • 解码到带类型的请求结构体中,然后在同一步骤中验证——解码成功不代表值有效。
  • 返回 400 和稳定的消息;绝不要回显解码器的错误文本。

4. URL 和命名约定

GET    /api/v1/users          → list users
POST   /api/v1/users          → create user
GET    /api/v1/users/{id}     → get user
PUT    /api/v1/users/{id}     → replace user
PATCH  /api/v1/users/{id}     → partial update
DELETE /api/v1/users/{id}     → delete user

GET    /api/v1/users/{id}/orders → list user orders (nested resource)

规则:

  • 资源使用复数名词:/users,而不是 /user
  • 多词路径使用 kebab-case:/order-items
  • 每个 API 只采用一种 JSON 字段命名约定,绝不混用——这里默认使用 camelCase("createdAt");如果已有 API,则应与其保持一致
  • 在 URL 路径中包含版本号:/api/v1/...
  • URL 中不使用动词:/users/search?q=alice,而不是 /searchUsers

5. 分页

type PageRequest struct {
    Cursor string `json:"cursor"`
    Limit  int    `json:"limit"`
}

type PageResponse[T any] struct {
    Items      []T    `json:"items"`
    NextCursor string `json:"nextCursor,omitempty"`
    HasMore    bool   `json:"hasMore"`
}

对于大型数据集,优先使用基于游标的分页,而不是 offset/limit。并发写入会破坏 offset 分页。

6. 优雅关闭

func main() {
    if err := run(context.Background()); err != nil {
        slog.Error("server stopped", slog.Any("error", err))
        os.Exit(1)
    }
}

func run(ctx context.Context) error {
    ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
    defer stop()

    srv := &http.Server{
        Addr:              ":8080",
        Handler:           router,
        ReadHeaderTimeout: 5 * time.Second, // Slowloris guard
        ReadTimeout:       10 * time.Second,
        WriteTimeout:      15 * time.Second,
        IdleTimeout:       120 * time.Second,
    }

    errc := make(chan error, 1)
    go func() { errc <- srv.ListenAndServe() }()

    select {
    case err := <-errc:
        return fmt.Errorf("listen: %w", err) // e.g. port already in use
    case <-ctx.Done():
    }

    // ctx is already cancelled; shutdown needs its own deadline.
    shutdownCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
    defer cancel()
    if err := srv.Shutdown(shutdownCtx); err != nil {
        return fmt.Errorf("shutdown: %w", err)
    }
    if err := <-errc; !errors.Is(err, http.ErrServerClosed) {
        return err
    }
    return nil
}
  • ReadHeaderTimeout 用于防御 Slowloris 攻击:没有它,客户端可以通过缓慢发送头部字节来保持连接打开。gosec 会将缺少该设置的服务器标记为 G112。
  • 使用 errors.Is 比较 http.ErrServerClosed,绝不要使用 ==。
  • 只能从 main 退出。在服务 goroutine 中调用 log.Fatal 会跳过所有延迟清理操作,包括关闭服务器本身。

7. 健康检查端点

// Liveness: is the process alive? GET /healthz → 200, no dependency checks
// Readiness: should it receive traffic? GET /readyz → 200 or 503
func (h *HealthHandler) handleReady(w http.ResponseWriter, r *http.Request) {
    if h.draining.Load() { // atomic.Bool, set when shutdown starts
        h.respondError(w, http.StatusServiceUnavailable, "shutting down")
        return
    }
    if err := h.db.PingContext(r.Context()); err != nil {
        h.respondError(w, http.StatusServiceUnavailable, "database unavailable")
        return
    }
    h.respondJSON(w, http.StatusOK, map[string]string{"status": "ready"})
}

在负载均衡器后收到 SIGTERM 时:设置 draining,等待一个就绪检查周期,让负载均衡器停止转发请求,然后调用 srv.Shutdown。先关闭会拒绝负载均衡器仍在发送的请求。保持存活检查不依赖其他服务——数据库故障不应导致编排器重启每个副本。

8. 错误响应格式

整个 API 使用一致的错误响应:

{
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "invalid request parameters",
        "details": [
            {"field": "email", "message": "must be a valid email"}
        ]
    }
}

在处理器边界将内部错误映射为 HTTP 状态码。绝不能向客户端泄露内部错误。对于标准格式,请使用 RFC 9457 问题详情(application/problem+json)——go-openapi 技能中提供了对应类型。

9. 幂等性和浏览器客户端

  • 创建资源或扣款的 POST 端点应接受 Idempotency-Key 头:在限定时间内存储键 → 响应;遇到重复键时重放已存储的响应,并对并发重复请求返回 409。客户端和代理会重试;没有此机制,重试就会导致重复扣款。
  • 使用 Cookie 调用 API 的浏览器需要 CSRF 防护——http.CrossOriginProtection(Go 1.25+);具体细节由 go-security-audit 技能负责。

评论

支持 Markdown