前言

标准库 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 篇。