下载工作台
Go 编程实战

net/http 标准库入门

试读上半部分 · 解锁后可读全文

第 12 章 · net/http、Handler 与 Server

本章目标:使用 http.ServeMuxhttp.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.Handlerengine.Use(mw)
请求体json.NewDecoder(r.Body).Decodec.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

字段与全站约定一致:slugis_publishedprice 为分

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 OKGET 成功
201 CreatedPOST 创建成功
400 Bad RequestJSON/校验失败
405 Method Not AllowedGET 用了 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

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。