6.8 KiB
第 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 逐行解析
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 示例:
server:
port: 8080
mode: debug
read_timeout: 10s
write_timeout: 10s
log:
level: debug
encoding: console
cors:
allow_origins: ["*"]
config.Load() 逻辑:
viper.SetConfigFile("config/config.yaml")+ReadInConfigviper.AutomaticEnv()环境变量覆盖(SERVER_PORT=9090可覆盖server.port)Unmarshal到Config结构体
新增配置:结构体加字段 → yaml 加默认值 → 代码中 cfg.NewField 使用。
7.4 日志:pkg/logger
// 初始化
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/errordefer logger.Sync()刷盘,避免日志丢失
7.5 路由与依赖注入:internal/router/router.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:
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
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
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
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
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 追加
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 动手练习
- 按上述步骤完整实现 Book 的 List/Update/Delete。
- 将 BookRepo 替换为 GORM + SQLite(项目已依赖
glebarez/sqlite)。 - 为 Book 补充
go test单测。