引言
在 2025 年初,我曾写过一篇《Go 项目实战:搭建高效的 Gin Web 目录结构》,为刚从 Java 转型到 Go、面对极简的 Gin 感到无从下手的开发者,提供了一套基于传统 MVC(Controller-Service-Model)的平滑过渡方案,那套架构帮助了很多伙伴建立了工程安全感。
然而,随着项目复杂度的演进以及对 Go 语言哲学的深入理解,我逐渐发现了传统水平分层架构在 Go 生态中的痛点:改一个需求需要在四五个目录下来回跳转、包名失去了原本的业务语义,仿佛还在走 Java 的那套路子...... Go 的核心哲学是显式、极简与高内聚。
这次,gin-pathway 想认真换个思路。我不再沿着传统 MVC 那套方式往前走,而是转向 Go 社区更常见的 Feature-Driven(业务驱动/特性驱动)架构,把同一块业务尽量收拢在一起,这样代码会更聚焦,改起来也更顺手。
传统 MVC 在 Go 中的痛点
有过 Java 开发经验的伙伴应该非常熟悉 SpringBoot 那套水平分层,但在 Go 中,强行套用 MVC 会引发两个核心问题:
- 高频跨目录切换(心智负担重) :假设要加一个"用户注册"功能,你需要在
model/user.go定义结构体,去repository/user.go写底层,去service/user.go写逻辑,最后去controller/user.go写接口。一个业务需求,代码被无情地"肢解"在完全不同的文件夹里。 - 包名(Package)失去语义 :在 Go 里,包名是代码的一部分(如
service.User)。如果所有业务的 Service 都堆在一起,这个包就变成了垃圾箱,失去了通过包名进行业务隔离的意义。
什么是 Feature-Driven 架构
与 MVC 这种"按技术角色水平分层"不同,Feature-Driven 提倡"按业务功能垂直切片"。它认为:一个业务模块(比如用户、订单)应该是一个独立的、自包含的"宇宙"。
顺着这个思路再往下走,如果还把系统底座和业务代码都塞进 internal,目录很快又会变乱。所以这次做 2.0 调整时,我把 biz(或者叫 modules)单独拎了出来,专门放业务模块。这样系统层做系统层的事,业务层也能更清楚地待在自己的位置上。
项目结构设计
全新升级后的 gin-pathway 2.0 目录结构如下:
html
├── /cmd
│ └── main.go
├── /configs
│ └── config.yaml
├── /internal
│ ├── /app # 系统底座:全局配置、DB初始化、服务启动
│ │ ├── bootstrap.go
│ │ ├── config.go
│ │ └── db.go
│ ├── /biz # 👈 核心业务宇宙(纵向切片)
│ │ └── /user # 独立的业务 Feature
│ │ ├── entity.go # 数据模型 (原Model)
│ │ ├── handler.go # 请求控制 (原Controller)
│ │ ├── repository.go # 数据访问 (原Repository)
│ │ ├── router.go # 模块自主路由
│ │ └── service.go # 核心业务 (原Service)
│ └── /middleware # 全局中间件
├── .env
├── go.mod
└── go.sum
目录职责
/internal/app:系统底座:负责加载配置、拉起数据库/缓存连接,并组装服务。除了核心运维和全局脚手架升级,平时开发基本不动这里。/internal/biz:业务核心:所有的业务需求全部收拢于此。/internal/biz/user:业务单元:关于用户相关的逻辑(Handler、Service、Repo、Entity)都放在同一个目录里,后续修改需求都在这里完成。
实战演练:2.0 的优雅落地
接下来,同样以一个最基础的"查询用户列表"为例,看看 2.0 的业务驱动架构如何清爽实现。
业务定义与存储:entity.go + repository.go
在 internal/biz/user/entity.go 中定义我们纯粹的数据结构:
go
package user
type User struct {
Id int64 `xorm:"pk autoincr 'id'"`
UserID int64 `xorm:"not null 'user_id'"`
UserName string `xorm:"varchar(30) 'user_name'"`
}
func (u User) TableName() string {
return "user"
}
在同目录下的 repository.go 中,直接实现数据的增删改查:
go
package user
import "github.com/go-xorm/xorm"
type UserRepository struct {
engine *xorm.Engine
}
func NewUserRepository(engine *xorm.Engine) *UserRepository {
return &UserRepository{engine: engine}
}
func (r *UserRepository) GetUsers() ([]*User, error) {
var users []*User
err := r.engine.Table(User{}.TableName()).Find(&users)
return users, err
}
核心业务与控制:service.go + handler.go
在 service.go 中承载业务逻辑,它直接持有同包下的 UserRepository:
go
package user
type UserService struct {
repo *UserRepository
}
func NewUserService(repo *UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUsers() ([]*User, error) {
return s.repo.GetUsers()
}
在 handler.go(原 Controller)中处理 HTTP 交互。注意,由于在同一个包下,我们可以直接使用 User 结构体,没有了繁琐的跨包导入:
go
package user
import (
"net/http"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
svc *UserService
}
func NewUserHandler(svc *UserService) *UserHandler {
return &UserHandler{svc: svc}
}
func (h *UserHandler) GetUsers(c *gin.Context) {
users, err := h.svc.GetUsers()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"})
return
}
c.JSON(http.StatusOK, gin.H{"users": users})
}
自包含的 router.go
这是 2.0 架构最优雅的地方。业务模块的路由应该由模块自己决定,而不是统一塞给外部。 我们在 router.go 中暴露一个显式的注册函数,在包内部完成整个依赖链的组装:
go
package user
import (
"github.com/gin-gonic/gin"
"github.com/go-xorm/xorm"
)
// RegisterRoutes 将用户模块的路由和依赖完全内聚
func RegisterRoutes(rg *gin.RouterGroup, engine *xorm.Engine) {
repo := NewUserRepository(engine)
svc := NewUserService(repo)
handler := NewUserHandler(svc)
userGroup := rg.Group("/user")
{
userGroup.GET("/", handler.GetUsers)
}
}
系统底座 bootstrap.go
因为路由和初始化逻辑已经被内聚到了各个业务包中,我们的核心启动引导类 bootstrap.go 变得无比干净利落。系统底座不需要知道什么是 Controller、什么是 Service,它只需要像拼积木一样把业务模块挂载上去:
go
package app
import (
"fmt"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"your_project/config"
"your_project/internal/biz/user" // 仅引入业务入口
)
func Start() {
// 1. 加载配置与初始化依赖 (省去常规逻辑...)
_ = config.LoadConfig()
_ = setupDependencies()
r := gin.Default()
// 2. 划分路由版本
v1 := r.Group("/api/v1")
// 3. 极其清爽的模块挂载
user.RegisterRoutes(v1, Engine)
// order.RegisterRoutes(v1, Engine) // 后续扩展新模块只需一行
_ = r.Run(fmt.Sprintf(":%d", config.Conf.App.Port))
}
总结
从 1.0 的"横向水平分层"到 2.0 的"纵向业务聚合",不仅是目录结构变了,更是开发思维的跃迁。
- 1.0 架构(MVC):适合小型项目,或者刚从 Java/PHP 转型过来、需要快速上手的伙伴。
- 2.0 架构(Feature-Driven) :适合团队长线维护、业务复杂的项目。它带来了极致的高内聚 ,并且由于每个模块都是自包含的,未来如果单体应用要演进为微服务 ,你只需要把
internal/biz/user整个文件夹复制出去,就能瞬间变成一个独立的服务,运维与重构体验极佳。
上述 2.0 架构的全新重构示例,已提交至 GitHub - vespeng/gin-pathway,欢迎大家 fork 体验更纯粹的 Go 哲学。