2. Go 语言 HTTP 服务基础:net/http 包快速搭建 RESTful API
在契约压测闭环中,HTTP 服务是数据传输的载体。Go 标准库 net/http 提供了构建 RESTful API 所需的核心能力,无需第三方框架即可完成路由注册、请求解析与 JSON 序列化。本章将围绕“契约数据如何通过 HTTP 传输”这一主线,讲解如何用最少的代码搭建一个可测试、可压测的 HTTP 服务。
2.1 核心组件与请求处理流程
一个典型的 Go HTTP 服务由三个核心组件构成:多路复用器(Mux)、处理器(Handler)和服务器(Server)。下图展示了请求从进入服务到返回响应的完整链路:
契约视角:在契约测试中,
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实现优雅关闭,保障契约测试的完整性。