HTTP 服务器模式:从入门到生产
“我的 HTTP 服务器能跑就行了,为什么要搞那么多花样?”
如果你还在用 http.ListenAndServe(":8080", mux) 一行代码打天下,那你一定还没经历过这些场景:
- 发布新版本时,正在处理的请求被粗暴中断,用户看到 502 错误
- 出了 bug 想查日志,几十行日志混在一起,分不清哪个请求对应哪条记录
- 一个慢查询拖垮了整个服务,所有请求都在排队等死
- 负载均衡器疯狂报警,因为健康检查接口返回了 500
这些都是"玩具服务器"和"生产级服务器"之间的差距。
今天这篇文章,我们就来一步步把一个简陋的 Hello World 服务器,改造成能在生产环境站稳脚跟的工业级服务。中间会穿插 Go 1.22 带来的路由增强特性——这可能是标准库路由多年来最大的一次进化。
Go 1.22 之前的路由痛点
在聊新模式之前,先说说老版本 http.ServeMux 的痛点。
Go 1.22 之前的 ServeMux 路由功能相当简陋:
- 只能按路径前缀匹配,不能精确匹配
- 不支持 HTTP 方法区分(GET、POST 等)
- 不支持路径参数(
/users/{id}) - 不支持通配符
这导致很多项目不得不引入 gorilla/mux、chi、gin 等第三方路由器。来看一个典型的"老式"代码:
// Go 1.21 及之前的写法
package main
import (
"net/http"
"strings"
"strconv"
)
func userHandler(w http.ResponseWriter, r *http.Request) {
// 手动区分 HTTP 方法
switch r.Method {
case "GET":
// 手动解析路径参数
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/users/"), "/")
if len(parts) == 0 || parts[0] == "" {
// 列出所有用户
listUsers(w, r)
return
}
id, err := strconv.Atoi(parts[0])
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
getUser(w, r, id)
case "POST":
createUser(w, r)
case "DELETE":
// 又要手动解析 id...
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/users", userHandler)
mux.HandleFunc("/users/", userHandler) // 注意这里要注册两次
http.ListenAndServe(":8080", mux)
}
是不是看得很累?这种代码在生产里到处都是,而且每个 handler 都在重复同样的"脏活累活"。
Go 1.22 增强路由:方法 + 路径 + 参数
Go 1.22 对 http.ServeMux 做了一次重大升级,带来了三个核心能力:
1. HTTP 方法匹配
现在你可以在路由模式里直接声明 HTTP 方法:
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
// ✅ 方法 + 路径,一目了然
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("PUT /users/{id}", updateUser)
mux.HandleFunc("DELETE /users/{id}", deleteUser)
http.ListenAndServe(":8080", mux)
}
func listUsers(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "listing users")
}
func createUser(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "creating user")
}
func getUser(w http.ResponseWriter, r *http.Request) {
// 通过 PathValue 提取路径参数
id := r.PathValue("id")
fmt.Fprintf(w, "getting user: %s\n", id)
}
func updateUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "updating user: %s\n", id)
}
func deleteUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "deleting user: %s\n", id)
}
2. 路径参数
{id}、{name} 这样的占位符直接出现在路由模式里,用 r.PathValue("id") 就能取值。再也不用手动 strings.Split 了。
3. 通配符
{path...} 可以匹配任意深度的路径:
// 匹配 /static/ 后面的任意路径
mux.HandleFunc("GET /static/{path...}", serveStatic)
func serveStatic(w http.ResponseWriter, r *http.Request) {
path := r.PathValue("path")
fmt.Fprintf(w, "serving: %s\n", path)
}
有了这三个能力,标准库的路由终于能"打"了。接下来我们就可以在这个基础上叠加生产级的各种模式。
生产级 HTTP 服务器全景
让我们先把整体架构看清楚,再逐个击破。一个生产级 HTTP 服务器通常包含这些层:
请求 → [Recovery] → [RequestID] → [Logging] → [Timeout] → [Router]
↓
响应 ← [Recovery] ← [RequestID] ← [Logging] ← [Timeout] ← [Handler]
中间件层层嵌套,请求穿过每一层,响应再穿回来。下面我们从外向内,逐个实现这些组件。
第一个模式:优雅停机(Graceful Shutdown)
这是生产服务器最重要的模式之一。当你要发布新版本时,不能直接 kill -9 把进程干掉,而是要:
- 停止接受新连接
- 等待正在处理的请求完成
- 超过超时时间才强制关闭
来看完整实现:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
// 模拟一个慢请求
time.Sleep(3 * time.Second)
fmt.Fprintln(w, "Hello, World!")
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// 在 goroutine 中启动服务器
go func() {
log.Printf("服务器启动在 %s", server.Addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("服务器启动失败: %v", err)
}
}()
// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
sig := <-quit
log.Printf("收到信号 %v,准备关闭服务器...", sig)
// 创建一个 30 秒的超时上下文
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 优雅关闭
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("服务器强制关闭: %v", err)
}
log.Println("服务器已优雅关闭")
}
运行这个程序,然后:
# 终端 1:发送一个慢请求
curl http://localhost:8080/hello
# 终端 2:在请求完成前发送 SIGINT
# 按 Ctrl+C
你会发现服务器会等待那个慢请求完成后才退出,而不是直接中断。这就是优雅停机的魅力。
为什么需要 ReadTimeout 和 WriteTimeout?
server := &http.Server{
ReadTimeout: 15 * time.Second, // 读取请求的超时
WriteTimeout: 15 * time.Second, // 写入响应的超时
IdleTimeout: 60 * time.Second, // Keep-Alive 连接的空闲超时
}
这三个超时是防止 Slowloris 攻击 的关键。攻击者会建立大量连接,但非常非常慢地发送数据,耗尽你的连接资源。不设超时,你的服务器就裸奔着。
第二个模式:Request ID 追踪
当你的服务部署在生产环境,一个用户的请求可能经过多个微服务。如果没有一个全局唯一的标识符,出了问题根本查不到完整的调用链。
package main
import (
"context"
"crypto/rand"
"encoding/hex"
"log"
"net/http"
)
// 定义 context key 类型(避免冲突)
type contextKey string
const requestIDKey contextKey = "request-id"
// RequestIDMiddleware 为每个请求生成唯一 ID
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 优先使用上游传来的 ID(微服务场景)
id := r.Header.Get("X-Request-ID")
if id == "" {
id = generateID()
}
// 放入 context
ctx := context.WithValue(r.Context(), requestIDKey, id)
// 回写到响应头(方便调试)
w.Header().Set("X-Request-ID", id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func generateID() string {
b := make([]byte, 8)
rand.Read(b)
return hex.EncodeToString(b)
}
// GetRequestID 从 context 中取出 request ID
func GetRequestID(ctx context.Context) string {
if id, ok := ctx.Value(requestIDKey).(string); ok {
return id
}
return "unknown"
}
func handler(w http.ResponseWriter, r *http.Request) {
id := GetRequestID(r.Context())
log.Printf("[%s] 处理请求: %s %s", id, r.Method, r.URL.Path)
fmt.Fprintf(w, "Request ID: %s\n", id)
}
用 crypto/rand 而不是 math/rand 是为了保证 ID 的全局唯一性。在高并发的生产环境里,math/rand 可能产生碰撞。
第三个模式:结构化日志中间件
fmt.Printf 打日志在生产环境是个灾难。你需要的是结构化的、包含 request ID 的日志:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
// LogEntry 结构化日志条目
type LogEntry struct {
Time string `json:"time"`
RequestID string `json:"request_id"`
Method string `json:"method"`
Path string `json:"path"`
Status int `json:"status"`
Duration string `json:"duration"`
UserAgent string `json:"user_agent"`
RemoteAddr string `json:"remote_addr"`
}
// responseWriterWrapper 捕获状态码
type responseWriterWrapper struct {
http.ResponseWriter
statusCode int
}
func (w *responseWriterWrapper) WriteHeader(code int) {
w.statusCode = code
w.ResponseWriter.WriteHeader(code)
}
// LoggingMiddleware 记录结构化日志
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 包装 ResponseWriter 以捕获状态码
wrapped := &responseWriterWrapper{
ResponseWriter: w,
statusCode: http.StatusOK,
}
next.ServeHTTP(wrapped, r)
entry := LogEntry{
Time: time.Now().Format(time.RFC3339),
RequestID: GetRequestID(r.Context()),
Method: r.Method,
Path: r.URL.Path,
Status: wrapped.statusCode,
Duration: time.Since(start).String(),
UserAgent: r.UserAgent(),
RemoteAddr: r.RemoteAddr,
}
// 输出 JSON 格式的日志
data, _ := json.Marshal(entry)
log.Println(string(data))
})
}
输出的日志长这样:
{
"time": "2023-02-28T16:10:00+08:00",
"request_id": "a1b2c3d4e5f6g7h8",
"method": "GET",
"path": "/users/42",
"status": 200,
"duration": "3.14ms",
"user_agent": "curl/7.79.1",
"remote_addr": "127.0.0.1:54321"
}
这种日志可以直接被 ELK、Loki 等日志系统解析和索引,搜索 request_id:"a1b2c3d4" 就能找到一次完整请求的所有相关日志。
第四个模式:超时控制
有些请求天生就慢——调用外部 API、查大表、跑复杂计算。如果不控制超时,一个慢请求就能拖垮整个服务。
package main
import (
"context"
"net/http"
"time"
)
// TimeoutMiddleware 给每个请求设置总超时
func TimeoutMiddleware(timeout time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
// 用新的 context 替换原来的
r = r.WithContext(ctx)
// 使用 http.TimeoutHandler 处理超时响应
http.TimeoutHandler(next, timeout, "request timeout").ServeHTTP(w, r)
})
}
}
// 一个模拟慢查询的 handler
func slowHandler(w http.ResponseWriter, r *http.Request) {
select {
case <-time.After(5 * time.Second):
w.Write([]byte("done"))
case <-r.Context().Done():
// context 被取消,及时退出
return
}
}
这里有个非常重要的细节:handler 必须监听 r.Context().Done()。如果 handler 里有个 for 循环不检查 context,超时机制根本不会生效——goroutine 还是会继续跑,只是响应已经被丢弃了。
第五个模式:Recovery 中间件
生产环境里,任何一个 goroutine 的 panic 都可能导致整个进程崩溃。Recovery 中间件能捕获 panic,记录堆栈,返回 500,但不会让进程挂掉:
package main
import (
"fmt"
"log"
"net/http"
"runtime/debug"
)
// RecoveryMiddleware 从 panic 中恢复
func RecoveryMiddleware(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\n%s", err, debug.Stack())
// 返回 500
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, `{"error":"internal server error"}`)
}
}()
next.ServeHTTP(w, r)
})
}
// 一个会 panic 的 handler
func panicHandler(w http.ResponseWriter, r *http.Request) {
panic("something went terribly wrong!")
}
⚠️ 注意:Recovery 中间件只能捕获当前 goroutine 的 panic。如果你的 handler 启动了新的 goroutine 并且里面 panic 了,它是抓不到的。所以,在 handler 里不要随意开 goroutine,除非你自己也做了 recover。
第六个模式:健康检查(Health Check)
Kubernetes、Nginx 负载均衡、Consul 服务发现……它们都需要一个接口来探测你的服务是否还"活着"。这个接口看起来简单,其实也有讲究。
健康检查通常分为两种:
- Liveness Probe:进程还在跑吗?最基本的存活检测。
- Readiness Probe:服务准备好接收流量了吗?可能依赖的数据库还没连上、缓存还没预热。
package main
import (
"encoding/json"
"net/http"
"sync/atomic"
"time"
)
// HealthStatus 健康状态
type HealthStatus struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
Uptime string `json:"uptime"`
Checks map[string]string `json:"checks,omitempty"`
}
// HealthChecker 健康检查器
type HealthChecker struct {
startTime time.Time
ready atomic.Bool
checks map[string]func() error
}
func NewHealthChecker() *HealthChecker {
return &HealthChecker{
startTime: time.Now(),
checks: make(map[string]func() error),
}
}
// SetReady 标记服务就绪
func (h *HealthChecker) SetReady(ready bool) {
h.ready.Store(ready)
}
// AddCheck 添加依赖检查
func (h *HealthChecker) AddCheck(name string, check func() error) {
h.checks[name] = check
}
// LivenessHandler 存活探针(始终返回 200)
func (h *HealthChecker) LivenessHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(HealthStatus{
Status: "alive",
Timestamp: time.Now().Format(time.RFC3339),
Uptime: time.Since(h.startTime).String(),
})
}
// ReadinessHandler 就绪探针(检查所有依赖)
func (h *HealthChecker) ReadinessHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if !h.ready.Load() {
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(HealthStatus{
Status: "not ready",
Timestamp: time.Now().Format(time.RFC3339),
})
return
}
results := make(map[string]string)
allHealthy := true
for name, check := range h.checks {
if err := check(); err != nil {
results[name] = "unhealthy: " + err.Error()
allHealthy = false
} else {
results[name] = "healthy"
}
}
status := "ready"
statusCode := http.StatusOK
if !allHealthy {
status = "degraded"
statusCode = http.StatusServiceUnavailable
}
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(HealthStatus{
Status: status,
Timestamp: time.Now().Format(time.RFC3339),
Uptime: time.Since(h.startTime).String(),
Checks: results,
})
}
使用方式:
func main() {
health := NewHealthChecker()
// 添加数据库检查
health.AddCheck("database", func() error {
return db.Ping()
})
// 添加 Redis 检查
health.AddCheck("redis", func() error {
return redisClient.Ping(context.Background()).Err()
})
// 初始化完成后标记就绪
health.SetReady(true)
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", health.LivenessHandler)
mux.HandleFunc("GET /readyz", health.ReadinessHandler)
// ... 其他路由
http.ListenAndServe(":8080", mux)
}
Kubernetes 配置示例:
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
第七个模式:中间件链
现在我们有了一堆中间件,怎么优雅地组合它们?
package main
import "net/http"
// Middleware 类型定义
type Middleware func(http.Handler) http.Handler
// Chain 将多个中间件按顺序组合
// 注意:最左边的中间件最外层,最先执行
func Chain(middlewares ...Middleware) Middleware {
return func(final http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
final = middlewares[i](final)
}
return final
}
}
func main() {
mux := http.NewServeMux()
// ... 注册路由
// 组装中间件链
chain := Chain(
RecoveryMiddleware, // 1. 最先执行:捕获 panic
RequestIDMiddleware, // 2. 生成 request ID
LoggingMiddleware, // 3. 记录日志
TimeoutMiddleware(30*time.Second), // 4. 超时控制
)
server := &http.Server{
Addr: ":8080",
Handler: chain(mux),
}
server.ListenAndServe()
}
执行顺序是这样的:
请求 → Recovery → RequestID → Logging → Timeout → Router → Handler
响应 ← Recovery ← RequestID ← Logging ← Timeout ← Router ← Handler
把它们全部组装起来
现在,让我们把所有模式整合成一个完整的生产级服务器:
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
// Server 生产级 HTTP 服务器
type Server struct {
httpServer *http.Server
health *HealthChecker
logger *log.Logger
}
func NewServer(addr string) *Server {
logger := log.New(os.Stdout, "[SERVER] ", log.LstdFlags)
health := NewHealthChecker()
mux := http.NewServeMux()
s := &Server{
health: health,
logger: logger,
}
// 注册路由
s.registerRoutes(mux)
// 组装中间件链
chain := Chain(
RecoveryMiddleware,
RequestIDMiddleware,
LoggingMiddleware,
TimeoutMiddleware(30 * time.Second),
)
s.httpServer = &http.Server{
Addr: addr,
Handler: chain(mux),
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
return s
}
func (s *Server) registerRoutes(mux *http.ServeMux) {
// 健康检查
mux.HandleFunc("GET /healthz", s.health.LivenessHandler)
mux.HandleFunc("GET /readyz", s.health.ReadinessHandler)
// API 路由
mux.HandleFunc("GET /api/v1/users", s.listUsers)
mux.HandleFunc("POST /api/v1/users", s.createUser)
mux.HandleFunc("GET /api/v1/users/{id}", s.getUser)
mux.HandleFunc("PUT /api/v1/users/{id}", s.updateUser)
mux.HandleFunc("DELETE /api/v1/users/{id}", s.deleteUser)
}
func (s *Server) listUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode([]string{"alice", "bob"})
}
func (s *Server) createUser(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"status": "created"})
}
func (s *Server) getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"id": id, "name": "Alice"})
}
func (s *Server) updateUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"id": id, "status": "updated"})
}
func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
// Start 启动服务器并监听关闭信号
func (s *Server) Start() error {
go func() {
s.logger.Printf("服务器启动在 %s", s.httpServer.Addr)
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
s.logger.Fatalf("服务器启动失败: %v", err)
}
}()
// 标记就绪
s.health.SetReady(true)
// 等待关闭信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
sig := <-quit
s.logger.Printf("收到信号 %v,开始优雅关闭...", sig)
s.health.SetReady(false) // 立即取消就绪状态
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := s.httpServer.Shutdown(ctx); err != nil {
return fmt.Errorf("服务器关闭失败: %w", err)
}
s.logger.Println("服务器已优雅关闭")
return nil
}
func main() {
server := NewServer(":8080")
if err := server.Start(); err != nil {
log.Fatal(err)
}
}
测试你的生产级服务器
别忘了写测试!Go 的 httptest 包让这变得很容易:
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestGetUser(t *testing.T) {
server := NewServer(":0") // 用 :0 让系统分配端口
req := httptest.NewRequest("GET", "/api/v1/users/42", nil)
rr := httptest.NewRecorder()
server.httpServer.Handler.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("status = %d; want %d", rr.Code, http.StatusOK)
}
if ct := rr.Header().Get("Content-Type"); ct != "application/json" {
t.Errorf("Content-Type = %s; want application/json", ct)
}
// 检查 X-Request-ID 是否被设置
if id := rr.Header().Get("X-Request-ID"); id == "" {
t.Error("X-Request-ID 未被设置")
}
}
func TestHealthCheck(t *testing.T) {
server := NewServer(":0")
server.health.SetReady(true)
tests := []struct {
name string
path string
wantCode int
}{
{"liveness", "/healthz", http.StatusOK},
{"readiness when ready", "/readyz", http.StatusOK},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", tt.path, nil)
rr := httptest.NewRecorder()
server.httpServer.Handler.ServeHTTP(rr, req)
if rr.Code != tt.wantCode {
t.Errorf("status = %d; want %d", rr.Code, tt.wantCode)
}
})
}
}
小结
今天我们系统学习了构建生产级 Go HTTP 服务器的核心模式:
- Go 1.22 增强路由:方法 + 路径 + 参数,标准库终于能打了
- 优雅停机:给正在处理的请求一个"善终"的机会
- Request ID:分布式追踪的基础设施
- 结构化日志:让日志变成可查询的数据
- 超时控制:防止慢请求拖垮整个服务
- Recovery:不要让一个 panic 毁掉整个进程
- 健康检查:告诉编排系统"我还能不能打"
- 中间件链:优雅地组合所有横切关注点
生产级服务器的核心原则:
- 永远设置超时,永远
- 每个请求都要有唯一的 ID
- 优雅停机不是可选项
- 健康检查要真实反映服务状态
- Panic 必须被捕获和记录
- 日志必须是结构化的、可搜索的
把这篇文章的代码抄进你的项目,稍微改改就能直接上生产。这不是玩具代码,这是真正经过实战检验的模式。
练习时间
- 路由改造:把你之前的一个项目改造成 Go 1.22 的路由模式
- 限流中间件:实现一个基于令牌桶的请求限流中间件
- CORS 中间件:实现一个支持配置的 CORS 中间件
- 指标收集:用 Prometheus 为每个中间件添加耗时指标
- 完整测试:为今天的所有中间件写单元测试
下一篇预告
下一篇文章,我们将深入 Go 1.23 的迭代器特性——range over func。这可能是 Go 语言自泛型以来最重要的语法扩展。它会彻底改变我们遍历数据的方式,让"惰性求值"在 Go 里成为现实。
我们下篇见!👋
参考资料:
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。