Chapter 10

实战与部署

把前九章的能力组装成完整 RESTful API,用 httptest 写测试,Docker 多阶段构建出极小镜像,并走一遍生产部署与性能优化清单。

10.1 实战部署名词

RESTful API
以资源为中心、用 HTTP 方法表达操作的接口风格,无状态、可缓存、统一接口。
httptest
Go 标准库测试工具,用 httptest.NewRecorder() 模拟 ResponseWriter,不用真起服务即可测 handler。
表驱动测试(Table-Driven Test)
Go 惯用测试写法:把用例组织成切片,循环跑同一断言逻辑,覆盖多分支。
多阶段构建(Multi-stage Build)
Dockerfile 分「构建阶段」与「运行阶段」,只把编译好的二进制拷进极小基础镜像,镜像可小到十几 MB。
静态编译
CGO_ENABLED=0 编译出不依赖 libc 的静态二进制,可跑在 scratch/alpine 等空镜像。
健康检查(Health Check)
/healthz 等探针接口,供 K8s liveness/readiness 或负载均衡判断实例是否可用。
pprof
Go 内置性能剖析工具,采集 CPU/内存/goroutine,定位瓶颈。

10.2 完整项目串讲

// internal/router/router.go —— 装配全部
func Setup(cfg *config.Config, db *gorm.DB, log *zap.Logger) *gin.Engine {
    r := gin.New()
    r.Use(middleware.Recovery(log), middleware.ZapLogger(log), middleware.CORS())

    userRepo := repository.NewUserRepo(db)
    userSvc := service.NewUserService(userRepo)
    userHdl := handler.NewUserHandler(userSvc)

    r.GET("/healthz", func(c *gin.Context) { c.String(200, "ok") })

    api := r.Group("/api/v1")
    api.POST("/login", userHdl.Login)
    api.POST("/users", userHdl.Register)

    auth := api.Group("", middleware.JWTAuth(cfg.JWT.Secret))
    {
        auth.GET("/users/:id", userHdl.Get)
        auth.GET("/users", userHdl.List)
        auth.DELETE("/users/:id", middleware.RequireRole("admin"), userHdl.Delete)
    }
    return r
}

10.3 httptest 单元测试

// handler/user_test.go
func setupRouter() *gin.Engine {
    gin.SetMode(gin.TestMode)
    r := gin.New()
    r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{"msg": "pong"}) })
    return r
}

func TestPing(t *testing.T) {
    r := setupRouter()
    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/ping", nil)
    r.ServeHTTP(w, req)

    if w.Code != 200 {
        t.Fatalf("want 200, got %d", w.Code)
    }
    if !strings.Contains(w.Body.String(), "pong") {
        t.Errorf("unexpected body: %s", w.Body.String())
    }
}
// 表驱动测试:一次覆盖多个用例
func TestCreateUserValidation(t *testing.T) {
    cases := []struct {
        name string
        body string
        want int
    }{
        {"合法", `{"name":"alice","email":"a@x.com","age":20}`, 201},
        {"缺邮箱", `{"name":"alice","age":20}`, 400},
        {"名字太短", `{"name":"a","email":"a@x.com"}`, 400},
    }
    r := setupRouter()
    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            w := httptest.NewRecorder()
            req, _ := http.NewRequest("POST", "/users", strings.NewReader(tc.body))
            req.Header.Set("Content-Type", "application/json")
            r.ServeHTTP(w, req)
            if w.Code != tc.want {
                t.Errorf("%s: want %d got %d", tc.name, tc.want, w.Code)
            }
        })
    }
}
service 层用接口 + mock

让 handler 依赖 service 接口而非具体实现,测试时注入 mock(gomock/mockery 生成),无需真连数据库即可测业务分支。集成测试再用 testcontainers 起真实 PG。

10.4 Docker 多阶段构建

# ---- 构建阶段 ----
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# 静态编译,去符号表减小体积
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/server

# ---- 运行阶段 ----
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY --from=builder /server /app/server
COPY configs ./configs
EXPOSE 8080
ENTRYPOINT ["/app/server"]
$ docker build -t myapp:latest .
$ docker run -p 8080:8080 -e JWT_SECRET=xxx myapp:latest
# 最终镜像 ~15MB(静态二进制 + alpine)

10.5 部署 Checklist

类别检查项
模式GIN_MODE=release,关闭 debug 输出
配置密钥/DSN 走环境变量或 Secret,不进镜像
健康检查/healthz 就绪探针,K8s liveness/readiness
优雅关机捕获 SIGTERM,Shutdown 超时兜底
反向代理Nginx/Traefik 终止 TLS,转 X-Real-IP,配 SetTrustedProxies
限流/超时ReadTimeout/WriteTimeout,接口限流
可观测结构化日志 + Prometheus 指标 + 链路追踪
资源容器 CPU/内存 limit,连接池匹配 DB 上限

10.6 性能优化

// 信任代理:正确解析客户端 IP,避免伪造
r.SetTrustedProxies([]string{"10.0.0.0/8"})

// 生产 HTTP Server 超时(防慢连接耗尽资源)
srv := &http.Server{
    Addr:         ":8080",
    Handler:      r,
    ReadTimeout:  10 * time.Second,
    WriteTimeout: 30 * time.Second,
    IdleTimeout:  60 * time.Second,
}

// pprof 剖析(仅内网开放)
import _ "net/http/pprof"
// go tool pprof http://localhost:8080/debug/pprof/profile
基准测试驱动优化

别凭感觉优化。用 go test -bench、wrk/vegeta 压测,先用 pprof 找到真正的瓶颈再动手。Gin 本身极快,绝大多数性能问题在数据库、序列化或外部调用上。

10.7 全教程总结