HTTP 与路由

没有 Web 框架,只有标准库 net/http 加一层约定:handler 返回 (any, error),成功由框架渲染,失败由 errs 映射成状态码。

handler 约定

type HandlerFunc func(ctx context.Context, r *http.Request) (any, error)

func (h *Handler) Greet(ctx context.Context, r *http.Request) (any, error) {
    name := r.PathValue("name")            // Go 1.22 路径参数
    if name == "" {
        return nil, errs.Invalid(40001, "name is required")
    }
    resp, err := h.svc.Greet(ctx, name)
    if err != nil {
        return nil, err                       // errs 值直接返回,框架负责映射
    }
    return map[string]string{"greeting": resp.Greeting}, nil
}

返回值到响应的映射是固定的:

返回 响应
data, nil 200 {"code":0,"msg":"ok","data":data}
nil, nil 204,无响应体
nil, errs.NotFound(40401, …) 404 {"code":40401,"msg":"…","data":null}
nil, fmt.Errorf("plain") 500——非 errs 错误一律按内部错误处理,不泄露细节
handler 里永远不出现 w http.ResponseWriter 这是它能被单测、能被中间件包装、能统一加超时与日志的前提。 需要原生控制的场景用 HandleRaw(见下)。

注册路由

func (s *Service) BuildRouter(srv *httpx.Server) error {
    h := order.NewHandler(s.orders)

    srv.Handle("POST", "/api/v1/orders", h.Create)          // 公开
    srv.Use(auth.Middleware(s.verifier))                    // 从这里往下都受保护
    srv.Handle("GET", "/api/v1/orders/{id}", h.Get)
    srv.Handle("DELETE", "/api/v1/orders/{id}", h.Cancel)   // 重复 pattern 会 panic
    return nil
}

路由用 ServeMux 语法,注册重复 pattern 会在启动时 panic—— 路由冲突属于配置错误,应该立刻发现,而不是等某个请求走错分支。

两个中间件槽

flowchart LR
    REQ([请求]) --> GUARD["guard
panic 恢复 · max_body"] GUARD --> SH["SecureHeaders
默认安全响应头"] SH --> OUT["UseOuter 中间件
CORS · gzip · RealIP"] OUT --> MUX{{mux 路由匹配}} MUX --> WRAP["wrap
trace id · 超时 · 封套"] WRAP --> MW["Use 中间件
认证 · 限流 · 幂等"] MW --> H["Handler"] H --> RESP([响应封套]) classDef slot fill:#fdf6f0,stroke:#c8641f,color:#2b2722 class SH,OUT,MW slot
橙色是三个可插拔位置。SecureHeaders 与 guard 由框架固定, 其余两处按需挂载。
槽 签名 位置 典型用途
Use func(next HandlerFunc) HandlerFunc 封套内部 认证、限流、幂等、往 ctx 塞东西
UseOuter func(next http.Handler) http.Handler mux 外部 CORS、gzip、真实客户端 IP、改响应头

Use 走封套:中间件拿到已带 trace id 与超时的 ctx,要拒绝就直接返回 errs 值,不用碰 ResponseWriter:

func requireAPIKey(next httpx.HandlerFunc) httpx.HandlerFunc {
    return func(ctx context.Context, r *http.Request) (any, error) {
        if r.Header.Get("X-API-Key") != key {
            return nil, errs.Unauthorized(40101, "missing or invalid X-API-Key")  // → 401
        }
        return next(ctx, r)
    }
}

Use 只护它之后注册的路由,公开/受保护的边界靠书写顺序表达。 UseOuter 覆盖整个 mux,所以 /healthz、 /readyz、/metrics 与 HandleRaw 路由它都管得到。

默认安全响应头

httpx.New 在链首装好安全头,不用你写:

X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'
Referrer-Policy: no-referrer
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Strict-Transport-Security: max-age=31536000; includeSubDomains   # 仅 https
HSTS 只在 https 下发 明文 http 上发 HSTS 至少会被忽略;若浏览器此前在 localhost 上收到过, 还会被钉死在它并不提供的 https 上。所以判定依据是 TLS 或 X-Forwarded-Proto: https(TLS 终止在网关时)。

CORS:默认不发

同源接口不该邀请外部页面来读。CORSPolicy 需要一个显式白名单, 它总是在 SecureHeaders 之后,所以在白名单之外丢掉的只是 CORS 头, 不是安全头:

srv.UseOuter(httpx.CORSPolicy("https://app.example"))  // 精确来源;OPTIONS 预检自动应答
// srv.UseOuter(httpx.CORSPolicy("*"))                 // 任意来源,且不带 credentials

被拒绝时仍会输出 Vary: Origin——否则缓存会把 A 来源的响应用来回答 B 来源的请求。

需要原生控制时:HandleRaw

流式响应、自定义状态码(201/202/206)、文件下载、SSE 走这条路。它注册在 mux 上, 不经过封套与超时 select:

srv.HandleRaw("GET /raw/plain", func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    w.WriteHeader(http.StatusAccepted)
    _, _ = w.Write([]byte("ok\n"))
})

内置端点

路径 说明
GET /healthz存活探针,不查依赖
GET /readyz就绪探针,ping 已配置的基础设施 + 服务自己的 ReadyChecker
GET /metricsPrometheus,注入 metrics 后自动注册
GET /version 构建信息(service / go 版本 / 模块 / vcs revision),在 BuildRouter 之前注册,不受业务中间件影响
GET /debug/pprof/*标准库 profiler,默认关闭,需 http.pprof: true

请求的作用域

每个请求的 ctx 由框架构造,携带 trace id 与超时;它与连接的 ctx 是分离的:

ctx, cancel := context.WithTimeout(
    context.WithoutCancel(obs.WithTraceID(spanCtx, traceID)), cfg.Timeout)
defer cancel()

这样客户端断连不会被记成服务端超时(记 499),而 handler 还能把副作用完整写完—— 包括幂等结果的落盘,所以重试同一个请求能拿到真话(见 稳定性防护)。

相关示例