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 错误一律按内部错误处理,不泄露细节 |
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
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 /metrics | Prometheus,注入 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 还能把副作用完整写完—— 包括幂等结果的落盘,所以重试同一个请求能拿到真话(见 稳定性防护)。
相关示例
- examples/http:路由、封套、中间件、HandleRaw。
- examples/observability:日志、trace id、指标。