← 返回博客
go2026-09-12 20:30:062 分钟 · 451 0

Gin 分组路由与项目分层:controller/service/repository

用路由组管理版本前缀,再把项目拆成 controller、service、repository 三层,用构造函数做依赖注入,给一个完整的用户模块示例。

接口超过十个,单文件 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. 三层职责划分

目录干什么
controllerhandler/取参数、调 service、拼响应,不含业务
serviceservice/业务逻辑、事务编排
repositoryrepo/只和数据库打交道,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. 为什么不用全局变量

有人图省事把 dbsvc 写成包级全局变量,handler 直接引用。小项目没问题,代价是:单元测试没法注入假数据库,并发场景容易踩到未初始化的竞态。构造函数注入让每个依赖显式可见,测试时用 NewUserRepo(fakeDB) 即可替换。

下一篇在 handler 之上加 JWT 鉴权中间件,把 c.Set("uid", ...) 落进这套分层里。

相关推荐

本文为原创文章,采用CC BY-NC-SA 4.0协议授权,转载请保留署名与原文链接。原文链接:https://www.wxbuluo.com/article/181