Chapter 09

工程化

把单文件重构为分层架构,用 viper 管理配置、zap 做结构化日志、优雅关机保证请求不丢,并用 Swagger 自动生成接口文档。

9.1 工程化名词

分层架构(Layered Architecture)
把代码按职责切成 handler(HTTP)→ service(业务)→ repository(数据)三层,单向依赖,各层可独立测试。
DTO / Entity
DTO 是对外的请求/响应模型;Entity 是数据库模型。分开可避免把内部字段暴露给客户端。
viper
Go 主流配置库,支持 YAML/JSON/env/远程配置,能热加载、多来源合并、优先级覆盖。
zap
Uber 出品的高性能结构化日志库,零分配、JSON 输出,便于被 ELK/Loki 采集分析。
优雅关机(Graceful Shutdown)
收到退出信号后停止接收新请求,等待在途请求处理完再退出,避免连接被强制切断。
Swagger / OpenAPI
用注释生成 API 文档与在线调试页(swag + gin-swagger),前后端协作利器。
依赖装配(Wiring)
在入口把各层实例串起来,可手写,也可用 wire/fx 等 DI 框架自动生成。

9.2 推荐项目结构

myapp/
├── cmd/server/main.go        # 入口
├── internal/
│   ├── config/config.go      # viper 加载
│   ├── handler/user.go       # HTTP 层:绑定、调 service、响应
│   ├── service/user.go       # 业务逻辑
│   ├── repository/user.go    # 数据访问
│   ├── model/user.go         # Entity
│   ├── dto/user.go           # 请求/响应 DTO
│   ├── middleware/           # 中间件集合
│   └── router/router.go      # 路由装配
├── pkg/                      # 可复用工具(resp/errcode/log)
├── configs/config.yaml
├── go.mod
└── Dockerfile

9.3 viper 配置管理

# configs/config.yaml
server:
  port: 8080
  mode: release
database:
  dsn: "postgres://user:pass@localhost:5432/app"
jwt:
  secret: "${JWT_SECRET}"   # 敏感项走环境变量
type Config struct {
    Server   struct{ Port int; Mode string }
    Database struct{ DSN string }
    JWT      struct{ Secret string }
}

func Load(path string) (*Config, error) {
    v := viper.New()
    v.SetConfigFile(path)
    v.AutomaticEnv() // 环境变量覆盖
    if err := v.ReadInConfig(); err != nil {
        return nil, err
    }
    var cfg Config
    if err := v.Unmarshal(&cfg); err != nil {
        return nil, err
    }
    return &cfg, nil
}

9.4 zap 结构化日志中间件

func ZapLogger(logger *zap.Logger) gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()
        logger.Info("request",
            zap.String("method", c.Request.Method),
            zap.String("path", c.Request.URL.Path),
            zap.Int("status", c.Writer.Status()),
            zap.Duration("latency", time.Since(start)),
            zap.String("ip", c.ClientIP()),
        )
    }
}
结构化 > 拼字符串

zap 的 zap.String("k", v) 输出 JSON 键值,方便 Loki/ELK 按字段检索与告警。别用 fmt.Sprintf 把一切塞进一行文本,那样几乎没法做聚合分析。

9.5 优雅关机

func main() {
    cfg, _ := config.Load("configs/config.yaml")
    gin.SetMode(cfg.Server.Mode)
    r := router.Setup(cfg)

    srv := &http.Server{
        Addr:    fmt.Sprintf(":%d", cfg.Server.Port),
        Handler: r,
    }

    // 后台启动
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("listen: %v", err)
        }
    }()

    // 等待中断信号
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("shutting down...")

    // 给在途请求 10 秒处理时间
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalf("forced shutdown: %v", err)
    }
    log.Println("server exited")
}

9.6 Swagger 接口文档

# 安装工具
$ go install github.com/swaggo/swag/cmd/swag@latest
$ go get github.com/swaggo/gin-swagger github.com/swaggo/files
// 在 handler 上写注释
// @Summary 获取用户
// @Tags user
// @Param id path int true "用户ID"
// @Success 200 {object} dto.UserResp
// @Router /users/{id} [get]
func (h *UserHandler) Get(c *gin.Context) { /* ... */ }

// 生成 docs 后挂载
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// 访问 http://localhost:8080/swagger/index.html

执行 swag init 扫描注释生成 docs/,即可得到在线可调试的 API 文档。

9.7 小结