使用 Go 标准库构建 HTTP 服务

用 net/http、encoding/json 与 httptest 实现可测试的最小 JSON 服务;配置超时并接到中间件与优雅关闭。

#type / howto #status / growing #tech / dev / backend #resource / go #protocol / http

[!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())
}

关注点:

  1. 不用默认全局 mux 当唯一结构——newMux 可测、可替换。
  2. ReadHeaderTimeout 防御慢速读头。
  3. 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/httphttptestGo 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 实现 ResponseWriterServeHTTP 同步执行——单元测试不绑端口

设计动机

  1. 先掌握标准库边界,框架只是糖。
  2. 可测性从第一天就有。
  3. 扩展路径清晰:中间件 → 分层 → 探针 → 关闭。

边界情况与反直觉行为

1. 先写 header 再失败

WriteHeader/Encode 后难以改状态码;可能失败的工作尽量在首次写入前完成。

2. 方法路由

Go 1.22 前 mux 不匹配方法;升级前后测试要一致。

3. ErrServerClosed

ShutdownListenAndServe 返回 http.ErrServerClosed,不应当致命乱退。

4. 客户端默认值

http.DefaultClient 无超时——这是客户端故事,但常与服务端示例一起被误用。

常见误区

[!warning] 常见误区:只 curl 不写测试
回归靠 httptest

[!warning] 常见误区:业务全写在 main
抽出 newMux/handler,main 只组装。

[!warning] 常见误区:忽略所有超时字段
至少设置 ReadHeaderTimeout

与相邻概念对比

概念差异
Gin/Echo路由/绑定糖多;底层仍是 Handler
gRPC另一套契约与传输
CLI 实践同样“核心可测 + 薄 main”

工程实践

扩展路径(同一骨架)

  1. JSON POSTDecode + 校验
  2. 中间件:日志/recover 包在 mux 外 → go-http-middleware
  3. 优雅关闭:signal + Shutdowngo-graceful-shutdown
  4. 分层:handler 调 service → go-http-handler-service-repository
  5. 探针/healthz vs /readyzgo-health-check-endpoint
  6. 容器:多阶段镜像 → 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。
  • 下一步:中间件与分层;健康检查与容器化。

自测题

概念题

  1. 为何测试不必 Listen
  2. ReadHeaderTimeout 主要防什么?
  3. 为什么推荐 newMux() 而不是只往 http.DefaultServeMux 注册?

代码推理题

handler 里先 w.Write([]byte("x")) 再想改成 500——通常能否成功?

工程思考题

如何把本示例演进为“注册用户”API 而不让 handler 膨胀?

参考答案

展开
  1. 直接 ServeHTTP 调用 handler 链。
  2. 慢客户端拖住连接读请求头(类 Slowloris)。
  3. 可测试、可多实例、不污染全局。
    代码题:通常不能可靠改状态码,已开始写 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本库相邻主题

创建于 2026/7/11 更新于 2026/7/15