第 12 章 · net/http、Handler 与 Server
本章目标:使用 http.ServeMux 与 http.Server 构建最小 REST API;理解 Handler 接口与 中间件 包装模式;处理 Method、Header、JSON 请求体与响应;对照 Gin 封装了哪些能力;为 gin-web api-go-demo 打基础;实现 toolkit-go 可选 /health 探针。
学时建议:3~4 小时(含 2 小时跟练)
前置:完成 go-dev ch11(文件 IO 与 JSON)。
12.1 场景说明:先懂标准库,再学 Gin
api-go-demo(gin-web 专栏)基于 Gin 框架,而 Gin 底层仍是 net/http。本章手写 HTTP 服务,理解路由、中间件、Context 的本质,再学 Gin 时只需关注 Engine、Binding、Middleware 链 等封装。
┌──────────────┐ ┌─────────────┐ ┌──────────────────┐
│ net/http │ ──► │ Gin / Echo │ ──► │ api-go-demo │
│ 标准库 │ │ Web 框架 │ │ 商品/订单 API │
└──────────────┘ └─────────────┘ └──────────────────┘
| 能力 | net/http(本章) | Gin(gin-web ch02+) |
|---|---|---|
| 路由 | ServeMux / Go 1.22+ 模式 | Engine + 路由树 |
| 路径参数 | r.PathValue("slug") | c.Param("slug") |
| 查询参数 | r.URL.Query().Get("published") | c.Query("published") |
| JSON 响应 | json.NewEncoder(w).Encode(v) | c.JSON(200, v) |
| 中间件 | 包装 http.Handler | engine.Use(mw) |
| 请求体 | json.NewDecoder(r.Body).Decode | c.ShouldBindJSON(&v) |
toolkit-go 本章 选修:启动 :9090/health 供 K8s 探针;主流程仍是 CLI(ch15)。
12.2 Handler 接口与 ServeMux
Go 1.22+ 起,ServeMux 支持 Method + 路径模式:
package main
import (
"encoding/json"
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", healthHandler)
mux.HandleFunc("GET /api/v1/products/{slug}", getProductHandler)
mux.HandleFunc("GET /api/v1/products", listProductsHandler)
mux.HandleFunc("POST /api/v1/products", createProductHandler)
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
log.Println("listening on :8080")
log.Fatal(srv.ListenAndServe())
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]string{
"status": "ok",
"service": "toolkit-go-probe",
})
}
Handler 接口:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
HandleFunc 将函数转为 Handler;任何实现 ServeHTTP 的类型均可挂载到 mux。
12.3 路径参数与商品 JSON
字段与全站约定一致:slug、is_published、price 为分。
func getProductHandler(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("slug")
if slug == "" {
http.Error(w, `{"error":"missing slug"}`, http.StatusBadRequest)
return
}
// 模拟数据 — 与 gin-web Product 对齐
p := map[string]any{
"slug": slug,
"name": "Go 开发手册",
"price": 6800,
"is_published": true,
"category_slug": "books",
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if err := json.NewEncoder(w).Encode(p); err != nil {
log.Printf("encode error: %v", err)
}
}
查询参数过滤:
func listProductsHandler(w http.ResponseWriter, r *http.Request) {
publishedOnly := r.URL.Query().Get("published") == "true"
products := []map[string]any{
{"slug": "go-handbook", "price": 6800, "is_published": true},
{"slug": "rust-guide", "price": 7200, "is_published": false},
}
if publishedOnly {
var filtered []map[string]any
for _, p := range products {
if p["is_published"].(bool) {
filtered = append(filtered, p)
}
}
products = filtered
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]any{"items": products})
}
12.4 POST JSON 与方法限制
func createProductHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", http.MethodPost)
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 限制 1MB
defer r.Body.Close()
var body struct {
Slug string `json:"slug"`
Name string `json:"name"`
Price int64 `json:"price"`
IsPublished bool `json:"is_published"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, `{"error":"invalid json"}`, http.StatusBadRequest)
return
}
if body.Slug == "" || body.Price < 0 {
http.Error(w, `{"error":"validation failed"}`, http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(body)
}
| 状态码 | 场景 |
|---|---|
| 200 OK | GET 成功 |
| 201 Created | POST 创建成功 |
| 400 Bad Request | JSON/校验失败 |
| 405 Method Not Allowed | GET 用了 POST 路由 |
| 500 Internal Server Error | 未 recover 的 panic |
12.5 中间件模式
Gin 的 Logger + Recovery 在标准库中手动实现:
type Middleware func(http.Handler) http.Handler
func withLogging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func withRecovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("panic: %v", rec)
http.Error(w, `{"error":"internal"}`, http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func withRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = idpkg.NewRequestID() // ch08 pkg/id