2. Go 语言 HTTP 服务基础:net/http 包快速搭建 RESTful API

在契约压测闭环中,HTTP 服务是数据传输的载体。Go 标准库 net/http 提供了构建 RESTful API 所需的核心能力,无需第三方框架即可完成路由注册、请求解析与 JSON 序列化。本章将围绕“契约数据如何通过 HTTP 传输”这一主线,讲解如何用最少的代码搭建一个可测试、可压测的 HTTP 服务。

2.1 核心组件与请求处理流程

一个典型的 Go HTTP 服务由三个核心组件构成:多路复用器(Mux)、处理器(Handler)和服务器(Server)。下图展示了请求从进入服务到返回响应的完整链路:

Go net/http 请求处理流程 Go net/http 请求处理流程 客户端请求 http.Server ServeMux 路由 业务 Handler 响应返回客户端 关键点: • ServeMux 根据 URL 路径匹配注册的 Handler • 每个 Handler 实现 ServeHTTP(ResponseWriter, *Request) 接口
契约视角:在契约测试中,ServeMux 的路由规则就是“契约端点”的映射。每个路由对应一个契约接口,Handler 负责将请求数据反序列化为契约结构体,并将响应序列化为 JSON 返回。

2.2 快速搭建 RESTful API 服务

以下代码演示如何用 net/http 搭建一个支持 CRUD 的用户管理 API。注意路由注册方式与 Handler 的写法:

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "strings"
)

// User 契约结构体
type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
    Age  int    `json:"age"`
}

// 模拟数据存储
var users = []User{
    {ID: 1, Name: "Alice", Age: 28},
    {ID: 2, Name: "Bob", Age: 32},
}

// 用户处理器:根据路径分发
func userHandler(w http.ResponseWriter, r *http.Request) {
    // 提取路径中的 ID 部分
    pathParts := strings.Split(strings.TrimPrefix(r.URL.Path, "/users/"), "/")
    id := pathParts[0]

    switch r.Method {
    case http.MethodGet:
        if id == "" {
            // GET /users — 返回全部用户
            w.Header().Set("Content-Type", "application/json")
            json.NewEncoder(w).Encode(users)
        } else {
            // GET /users/{id} — 返回单个用户
            // 此处省略 ID 查找逻辑
            w.WriteHeader(http.StatusNotImplemented)
        }
    case http.MethodPost:
        // POST /users — 创建用户
        var newUser User
        if err := json.NewDecoder(r.Body).Decode(&newUser); err != nil {
            http.Error(w, `{"error":"invalid JSON"}`, http.StatusBadRequest)
            return
        }
        newUser.ID = len(users) + 1
        users = append(users, newUser)
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(newUser)
    default:
        http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
    }
}

func main() {
    // 注册路由:匹配 /users/ 前缀
    http.HandleFunc("/users/", userHandler)
    // 健康检查端点
    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"status":"ok"}`))
    })

    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
路由匹配规则:http.HandleFunc 使用前缀匹配。注册 /users/ 后,/users/1、/users/abc 等路径都会被路由到同一个 Handler。在 Handler 内部通过 r.Method 区分 HTTP 方法,实现 RESTful 语义。

2.3 JSON 序列化与反序列化

契约测试中,请求体与响应体均以 JSON 格式传输。encoding/json 包提供了两个核心操作:

操作 函数/方法 使用场景 性能注意点
序列化 json.NewEncoder(w).Encode(v) 将结构体直接写入 HTTP 响应 流式写入,无需中间 buffer,适合大响应
反序列化 json.NewDecoder(r.Body).Decode(v) 从请求体读取 JSON 到结构体 流式读取,避免一次性加载整个 Body
内存序列化 json.Marshal(v) 需要先获取 []byte 再处理 产生额外内存分配,压测场景慎用
内存反序列化 json.Unmarshal(data, v) 从已存在的 []byte 解析 适合预读取 Body 后复用
压测陷阱:在压测场景下,json.Marshal 和 json.Unmarshal 会为每次调用分配新的内存 buffer,导致 GC 压力增大。优先使用 json.NewEncoder 和 json.NewDecoder 的流式接口,可减少内存分配次数,提升吞吐量。

2.4 路由处理进阶:自定义多路复用器

标准库 http.ServeMux 仅支持前缀匹配,无法直接实现 RESTful 路径参数(如 /users/:id)。在契约测试中,如果需要精确匹配路径参数,可以自定义多路复用器或使用第三方路由。以下是一个简单的路径参数提取实现:

// 自定义路由:支持路径参数
type ParamMux struct {
    routes map[string]http.HandlerFunc
}

func (m *ParamMux) HandleFunc(pattern string, handler http.HandlerFunc) {
    m.routes[pattern] = handler
}

func (m *ParamMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // 遍历注册的路由,尝试匹配
    for pattern, handler := range m.routes {
        if params, ok := matchPattern(pattern, r.URL.Path); ok {
            // 将参数注入请求上下文
            ctx := context.WithValue(r.Context(), "params", params)
            handler(w, r.WithContext(ctx))
            return
        }
    }
    http.NotFound(w, r)
}

// matchPattern 实现简单的 :id 匹配
func matchPattern(pattern, path string) (map[string]string, bool) {
    // 省略具体实现,返回参数映射
    return nil, false
}
契约测试建议:在契约测试阶段,建议使用标准库 http.ServeMux 保持最小依赖。如果路径参数是契约的一部分,可以在 Handler 内部通过 strings.Split 手动解析,避免引入第三方路由库带来的测试复杂度。

2.5 服务启动与优雅关闭

生产级服务需要支持优雅关闭,确保正在处理的请求完成后再退出。以下代码展示了如何结合 http.Server 和系统信号实现:

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/users/", userHandler)

    srv := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    // 启动服务
    go func() {
        log.Println("Server starting on :8080")
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("listen: %s\n", err)
        }
    }()

    // 等待中断信号
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("Shutting down server...")

    // 优雅关闭:等待 5 秒完成当前请求
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("Server forced to shutdown:", err)
    }
    log.Println("Server exited")
}
压测影响:优雅关闭机制在压测中至关重要。当压测工具持续发送请求时,服务关闭前会等待正在处理的请求完成,避免连接重置导致的数据丢失。契约测试中的“关闭行为”也应作为契约的一部分进行验证。

2.6 本章小结

  • 路由注册:http.HandleFunc 使用前缀匹配,适合 RESTful 资源路径。
  • JSON 处理:流式接口 json.NewEncoder/Decoder 是压测场景的首选,减少内存分配。
  • 路径参数:标准库不支持 :id 语法,需手动解析或自定义路由。
  • 服务生命周期:使用 http.Server.Shutdown 实现优雅关闭,保障契约测试的完整性。