3. HTTP 基础与 RESTful API:HTTP 方法、状态码、REST 设计原则、Go net/http 包快速入门

3.1 HTTP 协议核心要素

HTTP(HyperText Transfer Protocol)是 API 通信的基石。一个完整的 HTTP 请求-响应周期由请求行、请求头、请求体以及状态行、响应头、响应体构成。在契约校验中,我们最关注的是方法、路径、状态码和媒体类型。

3.2 HTTP 方法与 RESTful 语义

RESTful API 将 HTTP 方法映射为对资源的操作。下表列出核心方法及其在契约校验中的典型用途:

HTTP 方法 CRUD 语义 幂等性 请求体 响应体 契约校验关注点
GET 读取资源 是 无 资源表示 查询参数、响应结构
POST 创建资源 否 创建参数 新资源 / 操作结果 请求体 schema、201 响应
PUT 全量替换 是 完整资源 更新后资源 请求体必填字段、200/204
PATCH 部分更新 否 增量字段 更新后资源 字段可选性、JSON Merge Patch
DELETE 删除资源 是 无 空 / 确认消息 204 无内容、404 处理
契约校验提示:POST 是非幂等的,契约测试中应避免重复执行相同的 POST 请求导致数据重复。幂等方法(GET、PUT、DELETE)可以安全重放用于契约验证。

3.3 HTTP 状态码与契约约定

状态码是契约响应端的“第一行承诺”。在 API 契约中,每个端点必须明确声明可能返回的状态码及其对应的响应结构。以下是 RESTful API 中最常用的状态码分类:

  • 2xx 成功
    • 200 OK — GET、PUT、PATCH 成功返回资源
    • 201 Created — POST 成功创建资源,响应头应包含 Location
    • 204 No Content — DELETE 成功,或 PUT/PATCH 不返回 body
  • 3xx 重定向
    • 301 Moved Permanently / 308 Permanent Redirect — 资源 URL 变更
    • 304 Not Modified — 条件 GET 未变更
  • 4xx 客户端错误
    • 400 Bad Request — 请求体校验失败、参数格式错误
    • 401 Unauthorized — 缺少或无效认证
    • 403 Forbidden — 认证通过但无权限
    • 404 Not Found — 资源不存在
    • 409 Conflict — 资源冲突(如唯一约束)
    • 422 Unprocessable Entity — 语义错误(如校验失败)
  • 5xx 服务端错误
    • 500 Internal Server Error — 未预期异常
    • 503 Service Unavailable — 服务过载或维护
契约设计警告:不要在契约中声明 5xx 状态码作为正常响应路径。5xx 表示服务端故障,契约应保证在输入合法时只返回 2xx 或 4xx。将 5xx 纳入契约会模糊责任边界。

3.4 RESTful 设计原则

REST(Representational State Transfer)是一组架构约束,而非具体协议。在 API 契约设计中,以下原则直接影响接口结构:

  1. 资源导向 — URL 表示名词资源,而非动词操作。/users 而非 /getUsers。
  2. 无状态 — 每个请求包含所有必要信息,服务端不保存客户端会话状态。
  3. 统一接口 — 使用标准 HTTP 方法操作资源,使用标准媒体类型(如 application/json)表示资源。
  4. 超媒体驱动(HATEOAS) — 响应中包含链接,引导客户端发现下一步操作(高级实践,契约中可选)。
  5. 分层系统 — 客户端无需知道直接与哪一层通信(代理、网关、负载均衡)。

以下 SVG 展示了 RESTful API 请求从客户端到服务端的完整流转路径,突出契约校验的介入点:

RESTful API 请求流转与契约校验介入点 客户端 (Go / curl / 浏览器) 契约校验层 (请求体 / 响应体验证) 服务端 (业务逻辑 + 数据) HTTP 请求 转发请求 HTTP 响应 校验后响应 契约校验层职责 1. 验证请求方法是否与契约声明的端点方法匹配 2. 验证请求头(Content-Type、Accept)是否符合契约 3. 验证请求体 JSON 结构与契约 schema 一致 4. 验证响应状态码是否在端点声明的允许范围内 5. 验证响应体结构与契约 schema 一致

3.5 Go net/http 包快速入门

Go 标准库 net/http 提供了构建 HTTP 客户端和服务器的核心能力。在契约校验工具开发中,我们主要用它来发送请求和启动测试服务器。

3.5.1 启动一个简单的 HTTP 服务器

package main

import (
    "fmt"
    "net/http"
)

func main() {
    // 注册路由处理函数
    http.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case http.MethodGet:
            w.Header().Set("Content-Type", "application/json")
            w.WriteHeader(http.StatusOK)
            fmt.Fprint(w, `{"users":[]}`)
        case http.MethodPost:
            w.Header().Set("Content-Type", "application/json")
            w.WriteHeader(http.StatusCreated)
            fmt.Fprint(w, `{"id":"1","name":"alice"}`)
        default:
            w.WriteHeader(http.StatusMethodNotAllowed)
        }
    })

    fmt.Println("Server listening on :8080")
    http.ListenAndServe(":8080", nil)
}
关键点:http.HandleFunc 注册的处理器默认接收所有 HTTP 方法。在契约校验中,必须显式检查 r.Method 并返回正确的状态码,否则会导致契约测试误判。

3.5.2 发送 HTTP 请求(客户端)

package main

import (
    "fmt"
    "io"
    "net/http"
    "strings"
)

func main() {
    // GET 请求
    resp, err := http.Get("http://localhost:8080/api/users")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    fmt.Printf("Status: %d\n", resp.StatusCode)
    fmt.Printf("Body: %s\n", string(body))

    // POST 请求
    payload := `{"name":"bob"}`
    resp, err = http.Post(
        "http://localhost:8080/api/users",
        "application/json",
        strings.NewReader(payload),
    )
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    body, _ = io.ReadAll(resp.Body)
    fmt.Printf("Status: %d\n", resp.StatusCode)
    fmt.Printf("Body: %s\n", string(body))
}

3.5.3 使用 http.Client 进行精细控制

契约校验需要精确控制超时、重定向和请求头。推荐使用 http.Client 结构体:

client := &http.Client{
    Timeout: 5 * time.Second,
    // 禁止自动重定向,契约测试需要捕获原始响应
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
        return http.ErrUseLastResponse
    },
}

req, _ := http.NewRequest(http.MethodDelete, "http://localhost:8080/api/users/1", nil)
req.Header.Set("Authorization", "Bearer test-token")

resp, err := client.Do(req)
if err != nil {
    panic(err)
}
defer resp.Body.Close()

// 契约校验:检查状态码是否为 204
if resp.StatusCode != http.StatusNoContent {
    fmt.Printf("契约失败:期望 204,得到 %d\n", resp.StatusCode)
}
契约测试陷阱:默认的 http.Client 会跟随 3xx 重定向,导致最终状态码变为 200 而非 301/302。在契约校验中,务必设置 CheckRedirect 返回 http.ErrUseLastResponse 以获取原始重定向响应。

3.6 RESTful 路由设计模式

在 Go 中,net/http 默认的 ServeMux 不支持路径参数(如 /users/{id})。契约校验工具通常需要解析路径中的变量。以下是两种常见处理方式:

方案 实现方式 适用场景 契约校验兼容性
标准库 ServeMux 手动解析 URL 路径,如 strings.Split(r.URL.Path, "/") 小型工具、快速原型 需自行实现路径匹配逻辑
第三方路由(gorilla/mux) mux.Vars(r)["id"] 生产级 API 服务 路径参数提取方便,契约模板可直接映射
Go 1.22+ 增强 ServeMux http.HandleFunc("GET /api/users/{id}", handler) 新项目、标准库优先 原生支持路径参数,契约定义更简洁
课程实践建议:本课程后续章节的契约校验工具将基于 Go 1.22+ 的增强 ServeMux 构建,因为它原生支持路径参数和方法路由,减少第三方依赖,且与 OpenAPI 路径模板的映射更直接。

以下 SVG 展示了 Go net/http 包在契约校验工具中的核心组件关系:

Go net/http 在契约校验工具中的组件关系 契约校验引擎 http.Client http.Server 契约定义 (OpenAPI) 发送请求 启动测试服务 加载契约 契约校验引擎使用 http.Client 向目标服务发送请求,同时使用 http.Server 启动模拟服务进行消费者端契约测试。 契约定义(OpenAPI 规范)驱动请求构造和响应验证,三者通过 net/http 标准库无缝协作。

3.7 本章小结

  • HTTP 方法(GET/POST/PUT/PATCH/DELETE)对应 CRUD 操作,幂等性是契约测试中决定重放策略的关键属性。
  • 状态码是契约的“第一承诺”,每个端点必须声明允许的状态码集合,避免将 5xx 纳入正常契约路径。
  • RESTful 设计原则(资源导向、无状态、统一接口)直接影响 API 的 URL 结构和交互方式。
  • Go net/http 包提供了构建契约校验工具所需的核心能力:http.Client 用于发送请求,http.Server 用于启动模拟服务,http.HandleFunc 用于路由注册。
  • Go 1.22+ 的增强 ServeMux 原生支持路径参数和方法路由,是构建契约校验测试服务器的推荐选择。