http — Web 层与路由

没有框架,只有标准库 net/http 加一层约定:handler 签名是 func(ctx, *http.Request) (any, error),永远不碰 ResponseWriter。 这个示例把公开路由、受保护路由、路径参数、请求体解析、错误映射与逃生舱 HandleRaw 都放在一个 6 行左右的 BuildRouter 里, 看完你能照它搭出自己的服务骨架。

运行

go run ./examples/http        # = go run ./examples/http -conf examples/http/config.yaml

没有外部依赖,不需要 make dev。监听 :8080 (config.yaml 里的 http.addr),-conf 默认指向 examples/http/config.yaml。同端口还挂着一批内置端点: /healthz、/readyz、/metrics、/version。

它演示了什么

路由注册:公开在前,中间件在后

下面是 examples/http/main.go 的 BuildRouter 全文。 注意中间的注释分界线——srv.Use(withTag, s.requireAPIKey) 之后注册的两条路由才需要 API key,之前的三条加上 raw 路由都是公开的。

func (s *httpService) BuildRouter(srv *httpx.Server) error {
	// Outer middleware wraps the mux itself, so its header lands on every
	// response — including HandleRaw routes and the builtin probes.
	srv.UseOuter(serverHeader("matex/http-demo"))

	// Public routes: registered before any Use, so nothing guards them.
	srv.Handle("GET", "/api/v1/echo/{name}", s.echo)
	srv.Handle("GET", "/api/v1/nothing", nothing)
	srv.Handle("GET", "/api/v1/errors/{kind}", fail)
	// Escape hatch: full control over status/headers/body.
	srv.HandleRaw("GET /raw/plain", rawPlain)

	// Everything below this line runs through both middleware, outermost
	// first: tag the request, then demand the API key.
	srv.Use(withTag, s.requireAPIKey)
	srv.Handle("POST", "/api/v1/users", createUser)
	srv.Handle("GET", "/api/v1/admin/whoami", whoami)
	return nil
}

两个中间件写着不一样的样子

同一份文件里两种中间件的对照。重点是签名与它们能拿到的东西: Use 那侧没有 ResponseWriter,拒绝请求就是返回一个 errs 值;UseOuter 那侧是原生 http.Handler, 所以能改响应头,也因此能覆盖探针路由。

// serverHeader is an outer middleware: it needs the ResponseWriter, so it
// lives outside the mux rather than in the envelope chain.
func serverHeader(v string) httpx.OuterMiddleware {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("X-Served-By", v)
			next.ServeHTTP(w, r)
		})
	}
}

// withTag is an envelope middleware: it enriches the request context so
// the handler can read the value. No ResponseWriter in sight.
func withTag(next httpx.HandlerFunc) httpx.HandlerFunc {
	return func(ctx context.Context, r *http.Request) (any, error) {
		return next(context.WithValue(ctx, tagKey{}, r.Header.Get("X-Tag")), r)
	}
}

守卫中间件同样只有几行,它把「未配置即关闭」当作约定: 配置里 api_key 为空字符串时检查被跳过。

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

请求在链上的位置

flowchart LR
    REQ([请求]) --> G["guard
panic 恢复 · max_body"] G --> SH["SecureHeaders
默认安全响应头"] SH --> OUT["UseOuter
X-Served-By"] OUT --> MUX{{ServeMux 路由匹配}} MUX --> W["wrapper
trace id · 超时 · 封套"] W --> MW["Use
withTag → requireAPIKey"] MW --> H["handler
echo / createUser / whoami"] H --> RESP([封套响应]) MUX -.-> RAW["HandleRaw
不走封套"] OUT -.-> P["/healthz /readyz /metrics /version"] classDef slot fill:#fdf6f0,stroke:#c8641f,color:#2b2722 class OUT,MW slot
橙色是两个可插拔位置。示例用的 X-Served-By 挂在 UseOuter 上, 所以连 /healthz 和 /raw/plain 都带它; withTag/requireAPIKey 在封套内,只管它们之后注册的业务路由。

验证

示例仓库里给出的 curl 与预期可以一一对上:

curl -i localhost:8080/api/v1/echo/world
# 200 {"code":0,"msg":"ok","data":{"greeting":"Hello, world!"}}

curl -i localhost:8080/api/v1/nothing
# 204,无响应体

curl -i localhost:8080/api/v1/errors/notfound
# 404 {"code":40401,"msg":"user 42 not found","data":null}

curl -i -X POST localhost:8080/api/v1/users -d '{"name":"alice"}'
# 401 {"code":40101,"msg":"missing or invalid X-API-Key",...}

curl -i -X POST localhost:8080/api/v1/users -H 'X-API-Key: demo-key' -d '{"name":"alice"}'
# 200 {"code":0,"msg":"ok","data":{"email":"","id":1,"name":"alice"}}

curl -i localhost:8080/api/v1/admin/whoami -H 'X-API-Key: demo-key' -H 'X-Tag: alice'
# 200 data 里有 "tag":"alice" —— 中间件塞进 ctx 的值被 handler 读到了

curl -i localhost:8080/raw/plain
# 202 text/plain,"raw handler: full control over status/headers/body"

/api/v1/errors/{kind} 接受的 kind 值就是 errs 的那些类别: invalid、unauthorized、forbidden、notfound、 conflict、ratelimited、internal、unavailable、 timeout;其他值返回 400 {"code":40000}。 另外请求头 X-Request-ID 会被透传并写回响应头,不传则自动生成。

测试

main_test.go 用 srv.Handler() 拿到底层 http.Handler, 走完整 wrapper 但不占端口。它钉住的具体行为:

go test ./examples/http

相关

HTTP 与路由概念页 封套、中间件槽、安全响应头、CORS 与内置端点的完整规则。 错误模型 handler 返回的 errs 值如何变成状态码。 示例索引 全部 20 个可运行示例。

相关示例