打破传统 MVC:在 Go 中实践高内聚的业务驱动架构

引言

在 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 会引发两个核心问题:

  1. 高频跨目录切换(心智负担重) :假设要加一个"用户注册"功能,你需要在 model/user.go 定义结构体,去 repository/user.go 写底层,去 service/user.go 写逻辑,最后去 controller/user.go 写接口。一个业务需求,代码被无情地"肢解"在完全不同的文件夹里。
  2. 包名(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 哲学。

相关推荐
人生百态,人生如梦17 分钟前
每日论文解读(9.2)——SwarmBench:面向LLM Agent Swarm编排能力的多维评估基准与经验增强框架
架构·agent
用户3434612783202 小时前
.NET 中任务调度的底层机制与你可能不知道的隐藏陷阱
架构
咖啡无伴侣2 小时前
2. 从零搭建企业级 Monorepo 工程化模板:ESLint 10 (基础骨架)+ Prettier 配置与避坑指南
前端·架构
Dawson Zhu2 小时前
从虚拟内存到 Agent 记忆:把大模型的“脑补“变成“查表“
人工智能·语言模型·架构·aigc·agi
Raas1002 小时前
MAI Gateway(魔芋企业级AI网关)详解:AI网关在架构中的位置,一文读懂企业AI流量治理
大数据·人工智能·架构·gateway·ai网关·mai gateway
陈珙2 小时前
.NET AI 实战:用 MCP 把 Claude Code 变成数据库 DBA
架构·.net·技术
2601_962074583 小时前
大数据-260 实时数仓 - 项目背景与需求 实时数仓架构 需求分析 技术选型 逻辑架构
大数据·架构
AIOps打工人3 小时前
【AIOPS】当运维 Agent 开始自己学:证据补全才是自学习的真门槛
程序员·架构
8733 小时前
在 Uber 规模下高效运行软件工厂
人工智能·架构