一、为什么要做这件事?
用 Node.js 写后端时,我非常喜欢 NestJS。它不仅仅是一个框架,更像是一套强制性的工程规范:模块(Module)、控制器(Controller)、服务(Service)层次分明,依赖注入(DI)让代码解耦得非常漂亮。
转到 Go 之后,我发现生态里缺少这种“开箱即用”的结构化框架。Gin、Echo 很灵活,但一旦项目变大:
- ❌
main.go塞满了初始化代码。 - ❌ 包之间互相引用,甚至出现循环依赖。
- ❌ 路由定义分散在各个角落,找一个 API 像大海捞针。
我并不是要强行把 Go 写成 Java 或 Node,但我希望在 Go 项目中也能拥有清晰的模块边界和自动化的依赖管理。
于是,我基于 Gin(Web 框架)+ Uber Fx(依赖注入)+ Ent(ORM),摸索出了一套“NestJS 风格”的 Go 工程模板 —— gin-app。
二、工程全貌:目录结构一览
在深入代码之前,先看看这套“NestJS 风格”的 Go 项目目录结构长什么样。
cmd/
app/ # 应用启动入口
internal/
app/ # 服务初始化入口
config/ # 配置管理
modules/ # 业务模块(user、post、auth 等)
router/ # 基础路由定义层
middleware/ # HTTP 中间件(鉴权、日志、跨域等)
dto/ # 数据传输对象(请求/响应模型)
pkg/
logger/ # 日志模块
httpserver/ # http server 模块
postgres/ # PostgreSQL 客户端
redis/ # Redis 客户端
jwt/ # JWT 工具库
oauth2/ # OAuth2(GitHub / Google 登录)
ent/
schema/ # Ent ORM 模型定义
migrate/ # 数据库迁移文件
docs/ # Swagger 文档
💡 核心设计思路补充
这种结构并非简单的文件堆砌,它背后遵循了几个核心工程原则:
1. 按“业务域”拆分,而不是按“技术层”
在传统的 Go 开发中,我们习惯把所有 Controller 放一起,所有 Service 放一起。
- 痛点:当项目有 50 个功能时,
controllers/目录下会有 50 个文件,改一个功能要在三四个顶级目录间反复横跳。 - 方案:
internal/modules下的每个子目录(如user)都是一个自治单元。它包含了该业务所需的 Controller、Service 和 Repository。 - 体感:找功能 = 找文件夹。当你需要重构“用户模块”时,你的视野只需锁定在
modules/user即可。
2. internal 与 pkg 的严格边界
- internal:存放项目的核心业务逻辑。Go 强制规定
internal下的代码不能被外部项目引用。这保证了业务逻辑的私密性和安全性,防止你的业务代码被其他项目意外耦合。 - pkg:存放通用基础设施(如 Logger、Redis 封装)。这些代码是业务无关的,完全可以独立出来甚至抽取成公共仓库。
3. ent/ 带来的类型安全
项目使用了 Facebook 出品的 Ent 作为 ORM。它不像 Gorm 那样依赖反射,而是通过 ent/schema 生成纯 Go 代码。
- 所有的数据库操作都是强类型的,这意味着你在编写代码时,编译器就能帮你发现 SQL 字段写错的问题。
4. 路由注册的“去中心化”
注意看,internal/router 只负责 Gin 引擎的初始化和全局中间件配置,它并不关心具体的业务路由。
- 解耦:每个模块(如
user.module.go)利用 Fx 的特性,在自己的文件夹里定义路由。 - 好处:新增一个业务模块时,你不需要去修改全局的
router.go,真正做到了模块的即插即用。
三、核心架构:这是怎么跑起来的?
在这个项目中,我引入了 Uber 开源的 fx 库。很多人只把它当依赖注入工具,但在 gin-app 里,它是整个应用的“心脏”和“调度器” 。
1. 入口即编排 (cmd/app/main.go)
抛弃传统的一行行手动初始化,现在的启动入口是声明式的。
看看 internal/app/app.go,所有的组件(数据库、Redis、路由、业务模块)都像积木一样被 fx 组装:
// internal/app/app.go
func Run(cfg *config.Config) {
fx.New(
// 1. 注入全局配置
fx.Supply(fx.Annotate(cfg, fx.As(new(config.ConfigInterface)))),
// 2. 基础设施层 (Infrastructure)
// 这里的 NewLogger, NewPostgresDB 等都会自动执行并注册到容器中
fx.Provide(
logger.NewLogger, // 日志
postgres.NewPostgresDB, // 数据库 (Ent)
redis.NewRedis, // Redis
jwt.NewJWTManager, // JWT
router.NewRouter, // Gin 基础实例
router.NewAPIRouter, // 路由分组 (Public/Private)
server.NewHTTPServer, // HTTP Server
),
// 3. 业务模块层 (Modules)
// 类似于 NestJS 的 imports: [UserModule, AuthModule]
modules.APIModule,
// 4. 启动层 (Bootstrap)
// 只有被 Invoke 的函数才会被执行,这里显式启动 HTTP 服务和注册路由
fx.Invoke(
func(s *server.HTTPServer) {
// 启动服务 (生命周期管理)
s.Run()
},
),
).Run()
}
这一段代码解决了什么?
- 你不需要关心
NewPostgresDB需要什么参数,fx会自动从容器里找。 - 你不需要关心启动顺序,
fx会根据依赖关系自动构建图谱。
四、像 NestJS 一样写业务模块
这是我最喜欢的部分。我将传统的 MVC 目录打散,改为按业务域拆分。每个模块目录包含自己的一切。
目录结构如下:
internal/modules/user/
├── user.controller.go # 处理 HTTP 请求
├── user.service.go # 业务逻辑
├── user.repository.go # 数据交互
└── user.module.go # 模块定义 (核心!)
最大的特点是:拒绝按技术分层(Controller/Service 文件夹),而是按业务分模块(User/Auth 文件夹) 。
你看,当你想修改用户相关的功能时,只需要盯着 internal/modules/user 这一个文件夹就够了,不用在 controllers 和 services 文件夹之间来回横跳。
1. 模块定义 (user.module.go)
在 gin-app 中,一个文件就是一个独立的“插件”。
// internal/modules/user/user.module.go
var Module = fx.Module(
"user", // 模块名称
// 1. 提供本模块的能力 (Service, Controller, Repo)
fx.Provide(
NewUserRepository,
NewUserService,
NewUserController,
),
// 2. 注册本模块的路由
// 注意:这里我们请求了 api_router.APIRouterParams,这是系统提供的路由能力
fx.Invoke(func(
router api_router.APIRouterParams,
userController *UserController,
) {
// 公开路由
userGroup := router.Public.Group("/users")
{
userGroup.POST("", userController.Create)
}
// 私有路由 (需要鉴权)
privateGroup := router.Private.Group("/users")
{
privateGroup.GET(":id", userController.GetByID)
}
}),
)
2. 控制器实现 (user.controller.go)
控制器变得非常纯粹,它不需要知道 Service 是怎么来的,只要结构体里声明了,fx 就会填进去。
type UserController struct {
logger logger.Interface
svc UserService // 依赖 Service 接口
}
// 构造函数:Fx 会自动注入依赖
func NewUserController(logger logger.Interface, svc UserService) *UserController {
return &UserController{
logger: logger,
svc: svc,
}
}
func (c *UserController) GetByID(ctx *gin.Context) {
// 纯粹的业务调用
user, err := c.svc.GetByID(ctx, id)
if err != nil {
// 统一错误处理
response.Error(ctx, err)
return
}
response.Success(ctx, user)
}
🛠️ 你可以这样尝试
如果你想在本地体验这套结构,可以尝试:
- 在
modules下新建一个order文件夹。 - 按照
user的模式编写order.module.go。 - 在
internal/modules/modules.go中把order.Module挂载上去。 你会发现,整个过程不需要修改任何现有的业务代码,这种纵向扩展的丝滑感正是这套模板的精髓。
// internal/modules/modules.go
var APIModule = fx.Module(
"api",
// auth module
auth.Module,
// user module
user.Module,
// post module
post.Module,
// access-token module
accesstoken.Module,
// common module
common.Module,
// order module
order.Module
)
五、巧妙的路由设计:Public 与 Private
在 gin-app 里,我不想在每个 Controller 里手动加 Use(AuthMiddleware)。我在基础设施层把路由分成了两组:
// internal/router/api_router.go
type APIRouter struct {
fx.Out // 告诉 Fx 这是一个输出对象
// 使用 Named Tag 区分不同的路由组
Public *gin.RouterGroup `name:"api:public"`
Private *gin.RouterGroup `name:"api:private"`
}
func NewAPIRouter(app *gin.Engine, jwt jwt.JWTManager) APIRouter {
root := app.Group("/api/v1")
// 1. 公开组:无中间件
public := root.Group("/")
// 2. 私有组:挂载鉴权中间件
private := root.Group("/")
private.Use(middleware.Auth(jwt))
return APIRouter{
Public: public,
Private: private,
}
}
这样做的好处是: 业务模块开发时(如上面的 user.module.go),只需要选择要把路由挂在 router.Public 还是 router.Private 下,鉴权逻辑完全被封装在底层,业务层完全无感。
六、生命周期管理:优雅的启动与关闭
Fx 提供了 Lifecycle 钩子,这在 Go 服务中至关重要(比如等待数据库连接关闭后再停机)。
在 internal/app/app.go 中:
func startHTTPServer(
lc fx.Lifecycle,
sd fx.Shutdowner,
httpServer httpserver.Server,
logger logger.Logger,
) {
lc.Append(
fx.Hook{
// start
OnStart: func(ctx context.Context) error {
logger.Info("http server - Starting HTTP Server...")
// 异步启动,不阻塞主进程
go func() {
if err := httpServer.Start(); err != nil && err != http.ErrServerClosed {
logger.Errorf("http server - Start HTTP Server error: %v", err)
sd.Shutdown()
}
}()
logger.Info("http server - Listening on ", httpServer.GetAddress())
return nil
},
// stop
OnStop: func(ctx context.Context) error {
logger.Info("http server - Stopping HTTP Server...")
// 优雅关闭:处理完当前请求再退出
err := httpServer.Shutdown()
if err != nil {
return err
}
logger.Info("http server - Server - Shutting down")
return nil
},
},
)
}
这意味着当你按下 Ctrl+C 时,Fx 会按依赖的反向顺序(先停 HTTP 服务,再断数据库连接)依次执行清理工作,保证数据安全。
七、进阶思考:这不仅仅是 Gin + Ent
虽然这个项目叫 gin-app,但我必须强调:这套架构的灵魂是 Uber Fx 构建的依赖注入体系,而非具体的 Web 框架或 ORM。
在软件工程中,铁打的架构,流水的库。
1. 它是“框架无关”的 (Framework Agnostic)
Gin 和 Ent 只是我在这个模板中选择的“插件”。得益于 Fx 的模块化设计和 Go 的接口机制,你可以轻松地进行组件替换,而不用推翻重来:
- 不喜欢 Gin? 你完全可以将
pkg/httpserver和internal/router替换为 Echo 或 Fiber 的实现。只要保持fx.Provide的注入方式不变,业务层的Service和Repository代码甚至不需要改动。 - 习惯用 Gorm? 只需重写
pkg/postgres的初始化逻辑,将其注册到 Fx 容器中。Repository层直接注入*gorm.DB即可,模块定义的结构(Module -> Controller -> Service)依然稳固。
2. 真正的价值:标准化的“骨架”
这套模板最大的价值在于它定义了一套**“标准骨架”**:
- 它规定了代码放在哪里(目录结构)。
- 它规定了组件如何通信(依赖注入)。
- 它规定了应用如何启停(生命周期)。
无论你填充进这副骨架的是 Gin、Echo 还是 gRPC,是 MySQL 还是 MongoDB,项目的可维护性和清晰度都不会降低。这就是架构设计的魅力。
八、总结
这套架构不是为了炫技,而是为了解决我在真实开发中遇到的“乱”:
- 目录结构清晰:
modules目录下全是业务,pkg目录下全是通用工具。 - 依赖反转:彻底告别全局变量和复杂的初始化链。
- 高内聚:删除一个功能,只需删除对应的
module文件夹,不会残留路由配置。
它可以不适合极其简单的 CRUD 小工具,但如果你正准备开始一个中型 Go 后端项目,无论你最终选择 Gin 还是 Echo,这套基于依赖注入的“模块化思维”都能帮你构建出更健壮的系统。
Talk is cheap, star the code: 👇
GitHub 仓库:github.com/1111mp/gin-…
如果你也在做 Go 工程化探索,欢迎 Issue 或 PR 交流!