关于 go-api-design
面向 Go 开发者的 HTTP API 与处理器设计技能,适用于维护服务契约。内容涵盖标准库路由、验证、错误格式、分页、中间件和优雅关闭。
主要功能
- 01HTTP 处理器与 ServeMux 规范
- 02中间件顺序和请求验证
- 03URL、命名与分页指引
- 04统一错误响应模式
- 05优雅关闭与健康检查
适用场景
- 01为 Go HTTP 服务设计路由
- 02审查请求验证和错误响应
- 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