前言
标准库 net/http 就能写出生产级 Web 服务。这篇从路由到优雅关停走一遍,再聊什么时候需要框架。
一、五分钟起步
package main
import (
"encoding/json"
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "ok")
})
// JSON API
mux.HandleFunc("GET /api/users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // go1.22+ 原生路径参数!
json.NewEncoder(w).Encode(map[string]string{
"id": id, "name": "zy",
})
})
log.Println("listening :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
curl http://localhost:8080/healthz
curl http://localhost:8080/api/users/42
Go 1.22 后 ServeMux 支持方法限定 + 路径参数(GET /users/{id}),日常路由基本不用框架了。
二、请求与响应的完整姿势
type CreateUserReq struct {
Name string `json:"name"`
Age int `json:"age"`
}
mux.HandleFunc("POST /api/users", func(w http.ResponseWriter, r *http.Request) {
// 1. 解析 JSON body(限制大小防滥用)
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MB
var req CreateUserReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"bad json"}`, http.StatusBadRequest)
return
}
// 2. 校验
if req.Name == "" {
http.Error(w, `{"error":"name required"}`, http.StatusUnprocessableEntity)
return
}
// 3. 响应 JSON(统一头)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]any{
"id": 1, "name": req.Name,
})
})
读取请求的各部位:
r.URL.Query().Get("page") // ?page=2
r.Header.Get("Authorization") // 请求头
r.PathValue("id") // 路径参数
r.RemoteAddr // 客户端地址(注意代理场景读 X-Forwarded-For)
r.ParseForm(); r.FormValue("k") // 表单
三、中间件:洋葱模型
type Middleware func(http.Handler) http.Handler
func Chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- { // 倒序包裹
h = mws[i](h)
}
return h
}
// 日志中间件
func withLogging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r) // 调用下一层
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
// 恢复 panic(保命)
func withRecover(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %v", err)
http.Error(w, `{"error":"internal"}`, http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// 用法
handler := Chain(mux, withLogging, withRecover)
http.ListenAndServe(":8080", handler)
四、优雅关停(生产必备)
收到 SIGTERM 时:停止接收新请求 → 处理完存量请求 → 退出(K8s 滚动更新的前提):
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: Chain(mux, withLogging, withRecover),
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("listen: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
<-quit // 阻塞等到信号
log.Println("shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil { // 最多等25s
log.Printf("forced exit: %v", err)
}
}
五、调用外部 API(客户端)
client := &http.Client{Timeout: 5 * time.Second} // 必设超时!
req, _ := http.NewRequest("GET", "https://api.example.com/v1/x", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("call api: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return fmt.Errorf("api status %d: %s", resp.StatusCode, body)
}
var result Result
json.NewDecoder(resp.Body).Decode(&result)
六、结构化日志:slog(go1.21+)
import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)
slog.Info("user created",
"user_id", 42,
"ip", r.RemoteAddr,
)
// {"time":"...","level":"INFO","msg":"user created","user_id":42,"ip":"10.0.0.5:3342"}
JSON 日志可以直接进 ELK/Loki 分析,替代传统行式日志。
七、要不要用框架?
| 方案 | 适合 | 代表 |
|---|---|---|
| 纯标准库 | 中小服务、依赖洁癖、学习底层 | net/http (1.22+) |
| 轻路由器 | 要中间件生态但要轻 | chi(100% 兼容标准库接口) |
| 全家桶 | 快速开发 CRUD、团队协作 | gin(生态最大) |
// chi 风格示例(和标准库无缝)
r := chi.NewRouter()
r.Use(middleware.Logger, middleware.Recoverer)
r.Get("/users/{id}", getUser)
r.Group(func(r chi.Router) {
r.Use(requireAuth) // 一组路由统一加鉴权
r.Post("/api/users", createUser)
})
我的建议:先写纯标准库理解原理,业务提速再上 chi/gin——框架会换,标准库一辈子。
八、压测验证
go build -o app . && ./app &
wrk -t4 -c200 -d15s http://localhost:8080/healthz
# 单机几十万 QPS 的底子, 超过多数业务需求
小结
| 能力 | 标准库姿势 |
|---|---|
| 路由 | mux.HandleFunc("GET /x/{id}", h) |
| JSON | json.NewDecoder/Encoder + Content-Type |
| 中间件 | func(http.Handler) http.Handler 链 |
| 优雅关停 | srv.Shutdown(ctx) + 信号监听 |
| 客户端 | http.Client{Timeout} |
| 日志 | slog JSON |
本文是「Golang」系列第 8 篇。