# 第 7 章 实战:StreamBox 项目解析 > 学完本章你将:彻底看懂本项目分层架构,能独立新增一个 CRUD 模块,并理解配置/日志/优雅关闭等生产要素。 ## 7.1 项目结构总览 ``` StreamBox/ ├── cmd/server/main.go # 入口:配置→日志→路由→启动→优雅关闭 ├── config/ │ ├── config.go # Viper 加载逻辑 │ └── config.yaml # 默认配置 ├── internal/ │ ├── handler/ # HTTP 层:参数绑定、调用 service、返回 response │ ├── service/ # 业务层:核心逻辑,可被多 handler 复用 │ ├── repo/ # 数据层:DB/缓存/外部接口(当前为内存 demo) │ ├── model/ # 数据模型 │ ├── middleware/ # Recovery/ZapLogger/Cors │ └── router/router.go # 路由组装与依赖注入 ├── pkg/ │ ├── logger/ # Zap 封装 │ └── response/ # 统一 Result ├── api/ # Swagger/API 文档(待扩展) └── Makefile # run/build/tidy/fmt ``` 分层原则:`handler → service → repo` 单向依赖,`pkg` 为可复用基础库,`internal` 禁止外部 import。 ## 7.2 入口:cmd/server/main.go 逐行解析 ```go cfg, err := config.Load() // 1. 加载配置 logger.Init(cfg.Log.Level, cfg.Log.Encoding) // 2. 初始化日志 r := router.New(cfg) // 3. 构建 Gin 引擎 srv := &http.Server{Addr: addr, Handler: r} // 4. 用标准 http.Server 包装 go srv.ListenAndServe() // 5. goroutine 启动 <-quit // 6. 阻塞等待 SIGINT/SIGTERM srv.Shutdown(ctx) // 7. 10 秒优雅关闭 ``` 为什么不用 `r.Run()`:`r.Run()` 内部直接 `ListenAndServe` 且无法捕获信号做 `Shutdown`,会丢失正在处理的请求。生产务必用 `http.Server + Shutdown`。 ## 7.3 配置:config/config.go + viper `config.yaml` 示例: ```yaml server: port: 8080 mode: debug read_timeout: 10s write_timeout: 10s log: level: debug encoding: console cors: allow_origins: ["*"] ``` `config.Load()` 逻辑: 1. `viper.SetConfigFile("config/config.yaml")` + `ReadInConfig` 2. `viper.AutomaticEnv()` 环境变量覆盖(`SERVER_PORT=9090` 可覆盖 `server.port`) 3. `Unmarshal` 到 `Config` 结构体 新增配置:结构体加字段 → yaml 加默认值 → 代码中 `cfg.NewField` 使用。 ## 7.4 日志:pkg/logger ```go // 初始化 logger.Init(cfg.Log.Level, cfg.Log.Encoding) // 使用 logger.Log.Info("request", zap.String("path", path)) logger.Sugar.Infow("server starting", "addr", addr) ``` - `console` 适合开发,`json` 适合生产(便于 ELK 收集) - `level` 动态控制:debug/info/warn/error - `defer logger.Sync()` 刷盘,避免日志丢失 ## 7.5 路由与依赖注入:internal/router/router.go ```go func New(cfg *config.Config) *gin.Engine { gin.SetMode(cfg.Server.Mode) r := gin.New() r.Use(middleware.Recovery()) r.Use(middleware.ZapLogger()) r.Use(middleware.Cors(cfg.Cors)) health := handler.NewHealthHandler() r.GET("/health", health.Check) pingRepo := repo.NewPingRepo() pingSvc := service.NewPingService(pingRepo) pingHandler := handler.NewPingHandler(pingSvc) v1 := r.Group("/api/v1") { v1.GET("/ping", pingHandler.Ping) } return r } ``` 新增模块时复制这三行:`repo → service → handler`,然后挂到分组。 ## 7.6 Handler/Service/Repo 职责 - **handler**(`internal/handler/ping.go`):只做 HTTP 相关——取参、校验、调 service、组响应。不写业务逻辑。 - **service**(`internal/service/ping.go`):业务逻辑,可组合多个 repo,返回 `error` 由 handler 转为响应。 - **repo**(`internal/repo/ping.go`):数据访问,当前为内存实现,未来可替换为 GORM/SQLite。 示例 `PingService`: ```go type PingService struct { repo *repo.PingRepo } func (s *PingService) Ping() string { return s.repo.Ping() } ``` ## 7.7 完整 CRUD 实战(以 Book 为例) 按分层新增一个资源,需 4 个文件 + 1 处路由: **1. model** `internal/model/book.go` ```go package model type Book struct { ID uint `json:"id" gorm:"primaryKey"` Title string `json:"title" binding:"required"` Author string `json:"author" binding:"required"` } ``` **2. repo** `internal/repo/book.go` ```go package repo import "streambox/internal/model" type BookRepo struct { db map[uint]*model.Book; nextID uint } func NewBookRepo() *BookRepo { return &BookRepo{db: make(map[uint]*model.Book), nextID: 1} } func (r *BookRepo) Create(b *model.Book) *model.Book { b.ID = r.nextID; r.nextID++; r.db[b.ID] = b; return b } func (r *BookRepo) Get(id uint) (*model.Book, bool) { b, ok := r.db[id]; return b, ok } func (r *BookRepo) List() []*model.Book { // ... return nil } ``` **3. service** `internal/service/book.go` ```go package service import "streambox/internal/model" type BookService struct { repo *repo.BookRepo } func NewBookService(repo *repo.BookRepo) *BookService { return &BookService{repo: repo} } func (s *BookService) Create(b *model.Book) *model.Book { return s.repo.Create(b) } ``` **4. handler** `internal/handler/book.go` ```go type BookHandler struct { svc *service.BookService } func NewBookHandler(svc *service.BookService) *BookHandler { return &BookHandler{svc: svc} } func (h *BookHandler) Create(c *gin.Context) { var req model.Book if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()); return } book := h.svc.Create(&req) response.Success(c, book) } func (h *BookHandler) Get(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if b, ok := h.svc.Get(uint(id)); ok { response.Success(c, b) } else { response.Fail(c, 404, 404, "book not found") } } ``` **5. router** `internal/router/router.go` 追加 ```go bookRepo := repo.NewBookRepo() bookSvc := service.NewBookService(bookRepo) bookHandler := handler.NewBookHandler(bookSvc) v1.POST("/books", bookHandler.Create) v1.GET("/books/:id", bookHandler.Get) v1.GET("/books", bookHandler.List) ``` 重启 `make run`,`curl -X POST localhost:8080/api/v1/books -H 'Content-Type: application/json' -d '{"title":"Go","author":"A"}'` 验证。 > 替换为真实数据库时,只需将 `BookRepo` 内部改为 `*gorm.DB`,service/handler 无需改动,这就是分层的价值。 ## 7.8 统一响应与错误 所有 handler 均通过 `pkg/response` 返回,避免 `c.JSON` 散落各处,便于前端统一处理。 ## 7.9 动手练习 1. 按上述步骤完整实现 Book 的 List/Update/Delete。 2. 将 BookRepo 替换为 GORM + SQLite(项目已依赖 `glebarez/sqlite`)。 3. 为 Book 补充 `go test` 单测。