4. 同步通信与RESTful API
在微服务架构中,同步通信是最直接的交互模式。RESTful API 凭借其无状态、资源导向和统一接口的特性,成为服务间同步调用的首选协议。本章将深入剖析如何使用 Go 标准库 net/http 构建符合领域边界的 REST 服务,涵盖路由设计、请求/响应模型及状态码规范。
核心原则:REST 不是 URL 风格,而是资源状态转移。领域驱动设计中的聚合根(Aggregate Root)天然对应 REST 资源,每个聚合根实例即为一个资源标识符。
4.1 路由设计:从 ServeMux 到模式匹配
Go 1.22 之前的 net/http.ServeMux 仅支持路径前缀匹配,无法区分 HTTP 方法。Go 1.22 引入了增强模式匹配,支持方法限定和路径变量。
| 特性 | Go 1.21 及之前 | Go 1.22+ |
|---|---|---|
| 方法匹配 | 需手动检查 r.Method | "GET /orders" 直接匹配 |
| 路径变量 | 需第三方库(如 gorilla/mux) | "GET /orders/{id}" 原生支持 |
| 通配符 | 不支持 | "GET /files/{path...}" |
| 优先级 | 最长匹配 | 最具体匹配(含方法) |
领域驱动路由设计模式:每个领域聚合根对应一个资源路径,子资源表达领域内的值对象或实体集合。
// 订单领域路由注册示例
mux := http.NewServeMux()
// 订单聚合根集合
mux.HandleFunc("GET /api/v1/orders", listOrders)
mux.HandleFunc("POST /api/v1/orders", createOrder)
// 订单聚合根实例
mux.HandleFunc("GET /api/v1/orders/{orderId}", getOrder)
mux.HandleFunc("PUT /api/v1/orders/{orderId}", updateOrder)
mux.HandleFunc("DELETE /api/v1/orders/{orderId}", deleteOrder)
// 订单子资源:订单项(值对象集合)
mux.HandleFunc("GET /api/v1/orders/{orderId}/items", listOrderItems)
mux.HandleFunc("POST /api/v1/orders/{orderId}/items", addOrderItem)
领域边界警告:避免跨聚合根的直接子资源嵌套。例如
/users/{userId}/orders 应通过查询参数或独立资源表达,而非路径嵌套,以维护聚合边界。
4.2 请求/响应模型:序列化与反序列化
REST 服务的核心是资源表示。Go 中通常使用 JSON 作为序列化格式,encoding/json 标准库提供了完备的编解码能力。
4.2.1 请求反序列化
// 创建订单请求体
type CreateOrderRequest struct {
CustomerID string `json:"customer_id"`
Items []OrderItemDTO `json:"items"`
Currency string `json:"currency"`
}
type OrderItemDTO struct {
ProductID string `json:"product_id"`
Quantity int `json:"quantity"`
UnitPrice float64 `json:"unit_price"`
}
func createOrder(w http.ResponseWriter, r *http.Request) {
var req CreateOrderRequest
// 限制请求体大小,防止内存攻击
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MB
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // 严格模式:拒绝未知字段
if err := decoder.Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
return
}
defer r.Body.Close()
// 调用应用服务
order, err := orderService.CreateOrder(r.Context(), req)
if err != nil {
writeError(w, http.StatusUnprocessableEntity, err.Error())
return
}
writeJSON(w, http.StatusCreated, order)
}
4.2.2 响应序列化与统一封装
// 统一响应结构
type APIResponse struct {
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
TraceID string `json:"trace_id,omitempty"`
}
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
resp := APIResponse{
Data: data,
TraceID: getTraceIDFromContext(r.Context()),
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
// 记录日志,但响应已部分写入
log.Printf("json encode error: %v", err)
}
}
func writeError(w http.ResponseWriter, status int, message string) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
resp := APIResponse{
Error: message,
TraceID: getTraceIDFromContext(r.Context()),
}
json.NewEncoder(w).Encode(resp)
}
领域驱动设计实践:请求 DTO 应只包含领域所需的最小字段集,避免暴露内部实体结构。响应 DTO 应聚合领域数据,而非直接序列化实体。
4.3 状态码规范:语义精确性
HTTP 状态码是 REST API 语义的重要组成部分。在微服务通信中,精确的状态码能帮助调用方快速定位问题。
| 状态码 | 含义 | 领域场景示例 |
|---|---|---|
| 200 OK | 请求成功,响应体包含资源 | GET /orders/{id} 成功返回订单 |
| 201 Created | 资源创建成功 | POST /orders 成功创建订单 |
| 204 No Content | 操作成功,无响应体 | DELETE /orders/{id} 删除成功 |
| 400 Bad Request | 请求格式错误或校验失败 | JSON 解析失败、缺少必填字段 |
| 401 Unauthorized | 未认证或认证失败 | 缺少或无效的 JWT Token |
| 403 Forbidden | 已认证但无权限 | 非订单所属用户尝试修改订单 |
| 404 Not Found | 资源不存在 | GET /orders/{nonExistentId} |
| 409 Conflict | 资源当前状态冲突 | 订单已支付,尝试取消已发货订单 |
| 422 Unprocessable Entity | 请求语义正确但领域规则拒绝 | 库存不足、信用额度超限 |
| 429 Too Many Requests | 速率限制触发 | 客户端请求过于频繁 |
| 500 Internal Server Error | 服务器内部错误 | 数据库连接失败、未捕获异常 |
| 503 Service Unavailable | 服务暂时不可用 | 熔断器打开、依赖服务宕机 |
常见错误:不要对所有客户端错误都返回 400。领域规则校验失败(如库存不足)应使用 422,资源冲突应使用 409,权限不足应使用 403。精确的状态码能让调用方做出更合理的重试或降级策略。
4.4 中间件模式:横切关注点
在 net/http 中,中间件是包装 http.Handler 的函数,用于处理日志、认证、限流、追踪等横切关注点。
// 中间件类型定义
type Middleware func(http.Handler) http.Handler
// 链式组合中间件
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// 日志中间件
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 包装 ResponseWriter 以捕获状态码
lrw := &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(lrw, r)
log.Printf("[%s] %s %s %d %v",
r.Method, r.URL.Path, r.RemoteAddr, lrw.statusCode, time.Since(start))
})
}
// 认证中间件
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" || !validateToken(token) {
writeError(w, http.StatusUnauthorized, "missing or invalid token")
return
}
// 将用户信息注入上下文
ctx := context.WithValue(r.Context(), "user", extractUser(token))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// 使用示例
mux := http.NewServeMux()
mux.HandleFunc("GET /api/v1/orders", listOrders)
handler := Chain(mux, LoggingMiddleware, AuthMiddleware, CORSMiddleware)
server := &http.Server{Addr: ":8080", Handler: handler}
性能提示:中间件顺序影响性能。将限流、超时等快速失败的中间件放在外层,将日志等开销较大的中间件放在内层,避免对已拒绝的请求做无用日志记录。
4.5 错误处理与领域异常映射
领域层抛出的异常应映射为合适的 HTTP 状态码和错误响应。定义统一的领域错误类型有助于保持一致性。
// 领域错误类型
type DomainError struct {
Code string `json:"code"`
Message string `json:"message"`
Status int `json:"-"`
}
var (
ErrOrderNotFound = &DomainError{"ORDER_NOT_FOUND", "订单不存在", http.StatusNotFound}
ErrOrderAlreadyPaid = &DomainError{"ORDER_ALREADY_PAID", "订单已支付", http.StatusConflict}
ErrInsufficientStock = &DomainError{"INSUFFICIENT_STOCK", "库存不足", http.StatusUnprocessableEntity}
ErrUnauthorizedAccess = &DomainError{"UNAUTHORIZED", "无权限操作", http.StatusForbidden}
)
// 错误处理中间件
func ErrorHandlerMiddleware(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 recovered: %v", rec)
writeError(w, http.StatusInternalServerError, "internal server error")
}
}()
next.ServeHTTP(w, r)
})
}
// 应用服务中的错误返回
func (s *OrderService) GetOrder(ctx context.Context, id string) (*Order, error) {
order, err := s.repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, ErrOrderNotFound
}
return nil, fmt.Errorf("database error: %w", err)
}
return order, nil
}
4.6 测试 REST 端点
使用 net/http/httptest 标准库可以方便地对 REST 端点进行单元测试和集成测试。
func TestCreateOrderHandler(t *testing.T) {
// 准备测试用例
tests := []struct {
name string
body string
wantStatus int
}{
{"valid order", `{"customer_id":"c1","items":[{"product_id":"p1","quantity":2,"unit_price":10.5}]}`, http.StatusCreated},
{"missing customer_id", `{"items":[]}`, http.StatusBadRequest},
{"invalid json", `{invalid}`, http.StatusBadRequest},
{"empty items", `{"customer_id":"c1","items":[]}`, http.StatusUnprocessableEntity},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/v1/orders", strings.NewReader(tt.body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
// 假设已注册路由
createOrder(w, req)
if w.Code != tt.wantStatus {
t.Errorf("status = %d, want %d; body = %s", w.Code, tt.wantStatus, w.Body.String())
}
})
}
}
测试策略:对每个 REST 端点至少覆盖:正常路径(200/201)、客户端错误(400/404/409/422)、认证失败(401/403)、服务器错误(500)。使用表格驱动测试保持用例清晰。
4.7 生产级配置清单
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| ReadTimeout | 10s | 读取请求头的超时时间 |
| ReadHeaderTimeout | 5s | 读取请求头的超时时间 |
| WriteTimeout | 30s | 写入响应的超时时间 |
| IdleTimeout | 60s | Keep-Alive 连接空闲超时 |
| MaxHeaderBytes | 1MB | 请求头大小限制 |
| 请求体限制 | 1MB - 10MB | 使用 http.MaxBytesReader |
| 压缩 | gzip | 对大于 1KB 的响应启用 |
| 速率限制 | 100 req/s 每客户端 | 使用令牌桶算法 |
// 生产级 HTTP 服务器配置
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 10 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20, // 1MB
}
// 优雅关闭
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("server forced shutdown: %v", err)
}
领域驱动设计总结:RESTful API 是领域边界的自然映射。每个聚合根对应一个资源端点,每个领域操作对应一个 HTTP 方法。通过精确的状态码和统一的错误处理,服务间通信的语义清晰度将大幅提升,为后续的事件驱动架构奠定坚实的同步通信基础。