Go 中的 HTTP 基础:net/http 包简介、Handler 与 ServeMux、构建第一个 RESTful 端点

在 Go 后端开发中,net/http 标准库是构建 HTTP 服务的基石。它提供了从网络监听、请求解析到响应生成的全套能力,无需依赖第三方框架即可搭建生产级别的 API 服务。本章将深入剖析 net/http 的核心抽象——Handler 与 ServeMux,并手把手构建你的第一个 RESTful 端点。

1. net/http 包的核心抽象

net/http 包的设计遵循“接口即约定”的哲学,其核心由三个关键接口与结构组成:

组件 类型 职责
Handler interface 定义处理 HTTP 请求的统一契约:ServeHTTP(ResponseWriter, *Request)
ServeMux struct HTTP 请求多路复用器,根据 URL 路径将请求路由到对应的 Handler
Server struct 监听 TCP 端口、管理连接生命周期、调用 Handler 处理请求
设计启示: Go 的 HTTP 处理链是 Server → ServeMux → Handler 的线性管道。任何实现了 ServeHTTP 方法的类型都可以作为 Handler,这种“鸭子类型”使得中间件、路由组、拦截器都可以用统一接口组合。

2. Handler 接口详解

Handler 接口是 Go HTTP 世界的“万用插头”。其定义极其简洁:

type Handler interface {
    ServeHTTP(w http.ResponseWriter, r *http.Request)
}

两个参数分别代表:

  • w (ResponseWriter):用于构建 HTTP 响应的接口,支持写入状态码、Header 和 Body。
  • r (Request):包含客户端请求的全部信息——URL、Header、Body、查询参数、Cookie 等。

以下是一个自定义 Handler 的完整示例:

type HealthHandler struct{}

func (h *HealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"status": "healthy"}`))
}
常见陷阱: 在 ServeHTTP 中必须显式调用 w.WriteHeader() 设置状态码。如果先调用 w.Write(),Go 会自动写入 200 OK,但后续再调用 WriteHeader 将无效。建议始终在写入 Body 前明确设置状态码。

3. ServeMux:请求路由的核心

ServeMux 是 Go 标准库内置的 HTTP 请求路由器。它根据请求的 路径(Path)匹配已注册的 Handler,并遵循以下匹配规则:

注册模式 匹配行为 示例
/api/users 精确匹配路径 /api/users,不匹配子路径 匹配 /api/users,不匹配 /api/users/123
/api/users/ 前缀匹配,匹配所有以该路径开头的请求 匹配 /api/users/、/api/users/123、/api/users/123/orders
/ 默认兜底,匹配所有未被其他模式匹配的请求 匹配 /favicon.ico、/unknown 等

注册 Handler 到 ServeMux 的标准方式:

mux := http.NewServeMux()
mux.Handle("/api/health", &HealthHandler{})
mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
    // 使用 HandleFunc 可以避免定义单独的结构体
    w.Write([]byte("users endpoint"))
})
性能提示: ServeMux 的匹配算法基于 最长前缀匹配。当多个模式匹配同一个请求时,选择路径最长的模式。例如,同时注册 /api/ 和 /api/users/,请求 /api/users/123 会匹配后者。

4. 构建第一个 RESTful 端点

下面我们构建一个完整的 /api/tasks 端点,支持 GET(获取列表)和 POST(创建任务)两种方法。这是 RESTful API 的最小可行实现:

package main

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

type Task struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

var (
    tasks   = []Task{{ID: 1, Name: "学习 Go HTTP"}}
    nextID  = 2
    mu      sync.Mutex
)

func tasksHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    switch r.Method {
    case http.MethodGet:
        mu.Lock()
        defer mu.Unlock()
        json.NewEncoder(w).Encode(tasks)

    case http.MethodPost:
        var newTask Task
        if err := json.NewDecoder(r.Body).Decode(&newTask); err != nil {
            http.Error(w, `{"error":"invalid JSON"}`, http.StatusBadRequest)
            return
        }
        mu.Lock()
        newTask.ID = nextID
        nextID++
        tasks = append(tasks, newTask)
        mu.Unlock()

        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(newTask)

    default:
        http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
    }
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/tasks", tasksHandler)

    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}
RESTful 实践要点:
  • 使用 r.Method 区分 HTTP 方法,而非在 URL 中嵌入动作(如 /api/tasks/create)。
  • 始终设置 Content-Type 响应头,告知客户端返回数据的格式。
  • 使用 sync.Mutex 保护共享状态(如内存中的切片),避免并发读写冲突。
  • 返回合适的 HTTP 状态码:200 OK、201 Created、400 Bad Request、405 Method Not Allowed。

5. 请求处理流程可视化

下图展示了从客户端请求到 Handler 响应的完整链路,以及 ServeMux 在其中的路由决策过程:

Go HTTP 请求处理流程 Go HTTP 请求处理流程 客户端 http.Server ServeMux Handler ServeMux 路由决策逻辑 1. 提取请求路径(如 /api/tasks) 2. 遍历已注册模式,按最长前缀匹配原则查找 3. 若匹配到模式,调用对应的 Handler.ServeHTTP() 响应路径 ResponseWriter 写入 → Server 发送 → 客户端接收 关键点:Handler 不直接与客户端通信,所有 I/O 由 Server 管理;ServeMux 仅做路由,不修改请求/响应。

6. 启动与验证

将上述代码保存为 main.go,执行以下命令启动服务:

go run main.go
# 输出: Server starting on :8080

使用 curl 验证端点行为:

# 获取任务列表
curl -s http://localhost:8080/api/tasks
# 输出: [{"id":1,"name":"学习 Go HTTP"}]

# 创建新任务
curl -s -X POST http://localhost:8080/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"name":"学习 ServeMux"}'
# 输出: {"id":2,"name":"学习 ServeMux"}

# 再次获取列表,验证新增
curl -s http://localhost:8080/api/tasks
# 输出: [{"id":1,"name":"学习 Go HTTP"},{"id":2,"name":"学习 ServeMux"}]
下一步扩展思路: 当前端点将所有逻辑写在一个函数中。随着端点增多,建议按资源拆分 Handler(如 TaskHandler 结构体),并使用 http.HandlerFunc 适配器简化注册。后续章节将引入中间件模式,实现日志、鉴权、限流等横切关注点。