使用 Go 标准库构建 HTTP 服务
用 net/http、encoding/json 与 httptest 实现可测试的最小 JSON 服务;配置超时并接到中间件与优雅关闭。
[!info] 关联笔记
使用 Go 标准库构建 HTTP 服务
这个实践为什么会出现
会写 http.ListenAndServe(":8080", nil) 只是起点。工程最小闭环是:
- 明确路由与方法
- 稳定的 JSON 契约
- 不监听端口也能测 handler
- 为超时、中间件与
Shutdown留扩展点
标准库 net/http + encoding/json + httptest 足够完成这一闭环,再决定是否上框架。
[!abstract] 一句话理解 用
ServeMux注册 handler;用httptest断言状态码与 body;用http.Server显式配置读超时,生产再接信号与Shutdown。
目标:GET /health
返回 200 与 JSON {"status":"ok"};非 GET 返回 405。
完整可运行示例
package main
import (
"encoding/json"
"log"
"net/http"
"time"
)
type healthResponse struct {
Status string `json:"status"`
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
// Go 1.22+ 若用 "GET /health" 注册,方法已由 mux 约束;
// 兼容旧注册时仍可显式判断:
if r.Method != http.MethodGet {
w.Header().Set("Allow", http.MethodGet)
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(healthResponse{Status: "ok"}); err != nil {
// header 可能已发送;记录日志即可
log.Printf("encode health: %v", err)
}
}
func newMux() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", healthHandler) // Go 1.22+ 方法+路径
return mux
}
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: newMux(),
ReadHeaderTimeout: 5 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}
关注点:
- 不用默认全局 mux 当唯一结构——
newMux可测、可替换。 ReadHeaderTimeout防御慢速读头。- Handler 保持瘦,便于后续接 service。
使用 httptest 验证
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestHealthHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rr := httptest.NewRecorder()
newMux().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
if ct := rr.Header().Get("Content-Type"); ct != "application/json" {
t.Fatalf("content-type=%q", ct)
}
var body healthResponse
if err := json.Unmarshal(rr.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body.Status != "ok" {
t.Fatalf("body=%+v", body)
}
}
func TestHealthMethodNotAllowed(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/health", nil)
rr := httptest.NewRecorder()
// 直接打 handler 以验证 405 分支;1.22 mux 可能在路由层拒绝
healthHandler(rr, req)
if rr.Code != http.StatusMethodNotAllowed {
t.Fatalf("status=%d", rr.Code)
}
}
文档:net/http、httptest、Go 1.22 routing。
运行清单
go test .
go run .
curl -i http://localhost:8080/health
- GET 返回 200 与 JSON
- 测试不监听端口
- Server 配置了
ReadHeaderTimeout
核心概念与准确模型
Handler 接口
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
HandlerFunc 适配普通函数。中间件是 func(http.Handler) http.Handler。
Server vs ListenAndServe 包函数
| 方式 | 说明 |
|---|---|
http.ListenAndServe(addr, h) | 简写,超时默认弱 |
&http.Server{...} | 可配超时、基上下文、Shutdown |
生产服务应持有 *http.Server。
测试模型
httptest.ResponseRecorder 实现 ResponseWriter,ServeHTTP 同步执行——单元测试不绑端口。
设计动机
- 先掌握标准库边界,框架只是糖。
- 可测性从第一天就有。
- 扩展路径清晰:中间件 → 分层 → 探针 → 关闭。
边界情况与反直觉行为
1. 先写 header 再失败
WriteHeader/Encode 后难以改状态码;可能失败的工作尽量在首次写入前完成。
2. 方法路由
Go 1.22 前 mux 不匹配方法;升级前后测试要一致。
3. ErrServerClosed
Shutdown 后 ListenAndServe 返回 http.ErrServerClosed,不应当致命乱退。
4. 客户端默认值
http.DefaultClient 无超时——这是客户端故事,但常与服务端示例一起被误用。
常见误区
[!warning] 常见误区:只 curl 不写测试
回归靠httptest。
[!warning] 常见误区:业务全写在 main
抽出newMux/handler,main 只组装。
[!warning] 常见误区:忽略所有超时字段
至少设置ReadHeaderTimeout。
与相邻概念对比
| 概念 | 差异 |
|---|---|
| Gin/Echo | 路由/绑定糖多;底层仍是 Handler |
| gRPC | 另一套契约与传输 |
| CLI 实践 | 同样“核心可测 + 薄 main” |
工程实践
扩展路径(同一骨架)
- JSON POST:
Decode+ 校验 - 中间件:日志/recover 包在 mux 外 → go-http-middleware
- 优雅关闭:signal +
Shutdown→ go-graceful-shutdown - 分层:handler 调 service → go-http-handler-service-repository
- 探针:
/healthzvs/readyz→ go-health-check-endpoint - 容器:多阶段镜像 → go-docker-deployment
推荐 Server 字段起点
srv := &http.Server{
Addr: ":8080",
Handler: newMux(),
ReadHeaderTimeout: 5 * time.Second,
// ReadTimeout / WriteTimeout / IdleTimeout 按负载再调
}
可验证实验
实验 1:httptest 200
断言 status 与 JSON 字段。
实验 2:错误方法
POST /health 得 405 或路由层 404/405(依注册方式)。
实验 3:手动 curl
go run . 后 curl -i。
实验 4:Shutdown(扩展)
加 signal 处理,确认 docker stop/Ctrl+C 不炸半截请求(见优雅关闭笔记)。
本节总结
- 本质:标准库 HTTP 的最小可测服务骨架。
- 关键规则:可注入 mux、httptest、显式 Server 超时。
- 最易错:无测试、无超时、逻辑堆 main。
- 下一步:中间件与分层;健康检查与容器化。
自测题
概念题
- 为何测试不必
Listen? ReadHeaderTimeout主要防什么?- 为什么推荐
newMux()而不是只往http.DefaultServeMux注册?
代码推理题
handler 里先 w.Write([]byte("x")) 再想改成 500——通常能否成功?
工程思考题
如何把本示例演进为“注册用户”API 而不让 handler 膨胀?
参考答案
展开
- 直接
ServeHTTP调用 handler 链。 - 慢客户端拖住连接读请求头(类 Slowloris)。
- 可测试、可多实例、不污染全局。
代码题:通常不能可靠改状态码,已开始写 body。
工程题:引入 service/repo;handler 只 decode/encode 与映射错误。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| net/http | 标准库 | Server/Handler |
| httptest | 标准库 | 测试 |
| encoding/json | 标准库 | JSON |
| Routing enhancements | 官方博客 | Go 1.22 mux |
| go-nethttp · go-json-and-serialization · go-graceful-shutdown | 本库 | 相邻主题 |