接口超过十个,单文件 main.go 就撑不住了。这一篇给一套能直接抄的分层结构:路由组管前缀,handler 做参数转换,service 写业务,repository 碰数据库。
1. 路由组管版本和前缀
Group 不只是加前缀,还能挂组级中间件。版本迭代时老接口留在 v1,新接口开 v2:
v1 := r.Group("/api/v1") { user := v1.Group("/users") { user.GET("", userHandler.List) user.GET("/:id", userHandler.Get) user.POST("", userHandler.Create) } }
需要鉴权的组单独叠加:
auth := v1.Group("/users", AuthRequired()) { auth.PUT("/:id", userHandler.Update) auth.DELETE("/:id", userHandler.Delete) }
2. 三层职责划分
| 层 | 目录 | 干什么 |
|---|---|---|
| controller | handler/ | 取参数、调 service、拼响应,不含业务 |
| service | service/ | 业务逻辑、事务编排 |
| repository | repo/ | 只和数据库打交道,CRUD 封装 |
严格单向依赖:handler 调 service,service 调 repo,反向不调用。这样换数据库只在 repo 动,换接口协议只在 handler 动。
3. 目录结构
gin-demo/
├── main.go
├── router/
│ └── router.go
├── handler/
│ └── user.go
├── service/
│ └── user.go
├── repo/
│ └── user.go
└── model/
└── user.go
4. 用构造函数做依赖注入
Go 没有 Spring 那种容器,但用构造函数注入足够清晰。从里往外建:
// repo/user.go type UserRepo struct { db *gorm.DB } func NewUserRepo(db *gorm.DB) *UserRepo { return &UserRepo{db: db} } func (r *UserRepo) FindByID(id uint) (*User, error) { var u User if err := r.db.First(&u, id).Error; err != nil { return nil, err } return &u, nil }
// service/user.go type UserService struct { repo *UserRepo } func NewUserService(repo *UserRepo) *UserService { return &UserService{repo: repo} } func (s *UserService) Get(id uint) (*User, error) { return s.repo.FindByID(id) }
// handler/user.go type UserHandler struct { svc *UserService } func NewUserHandler(svc *UserService) *UserHandler { return &UserHandler{svc: svc} } func (h *UserHandler) Get(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) u, err := h.svc.Get(uint(id)) if err != nil { FailErr(c, err) return } OK(c, u) }
5. 在 router 里装配
依赖关系在 router.go 一处连起来,main 只调 router.Setup:
func Setup(db *gorm.DB) *gin.Engine { r := gin.New() r.Use(Recovery(), gin.Logger()) userRepo := repo.NewUserRepo(db) userService := service.NewUserService(userRepo) userHandler := handler.NewUserHandler(userService) v1 := r.Group("/api/v1") { ug := v1.Group("/users") ug.GET("/:id", userHandler.Get) ug.POST("", userHandler.Create) } return r }
main.go 只负责读配置、连库、把 db 传给 router.Setup、最后 r.Run()。所有装配逻辑集中在一处,加新模块就往 Setup 里加三段。
6. 为什么不用全局变量
有人图省事把 db、svc 写成包级全局变量,handler 直接引用。小项目没问题,代价是:单元测试没法注入假数据库,并发场景容易踩到未初始化的竞态。构造函数注入让每个依赖显式可见,测试时用 NewUserRepo(fakeDB) 即可替换。
下一篇在 handler 之上加 JWT 鉴权中间件,把 c.Set("uid", ...) 落进这套分层里。