使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)

项目地址:github.com/1111mp/gin-...


一、写在前面:这是一次真实的工程探索经历

这篇文章并不是一篇"架构设计说明文档",而更像是一次个人工程探索的记录

我在使用 Node.js 写后端服务的过程中,一直很喜欢 NestJS 那种工程化的代码组织方式:

  • 模块化结构清晰
  • 项目层次分明
  • 依赖关系明确
  • 工程规范天然统一

当项目规模变大时,这种结构非常"稳",不容易乱。

后来在转向 Go 做后端项目时,我发现 Go 生态并没有类似 NestJS 这种"开箱即用"的工程型框架。更多是:

  • Gin / Echo / Fiber 解决 Web
  • 各种库组合日志、配置、数据库、鉴权
  • 架构自由,但缺少统一范式

一开始我也尝试过很多组合方式,说实话:项目规模小的时候没问题,一旦项目开始复杂,结构很容易失控

  • 目录开始变乱
  • 模块边界开始模糊
  • 依赖开始互相引用
  • 测试越来越难写
  • 新人越来越难上手

于是我开始有意识地去思考一个问题:

Go 里能不能也做一套"工程级结构模板"?

不追求框架能力,而是解决工程组织问题。

在这个过程中,我也参考过一些优秀项目,比如:

👉 github.com/evrone/go-c...

它在分层思想和依赖方向控制上给了我很多启发,但整体更偏理论型架构设计,并不是我想要的那种"工程模板型结构"。

后来逐步尝试将 Gin + Uber Fx 组合在一起,一边写项目,一边调整结构,慢慢形成了一套相对稳定的工程组织方式,也就是现在的:

gin-app
github.com/1111mp/gin-...

它不是一个框架项目,更像是:

我在真实项目中一步步试出来的一套 Go 工程模板结构


二、我想解决的,其实不是"技术问题"

说实话,这个项目一开始并不是为了追求技术复杂度,而是为了解决几个很现实的问题:

  • 项目一大,结构就乱
  • 人一多,风格就乱
  • 模块多了之后,依赖就开始乱
  • 没有统一结构规范,重构成本极高

所以 gin-app 的核心目标其实很简单:

让 Go 项目在规模增长时,依然能保持结构清晰

而不是:

写代码更炫、技术更复杂


三、为什么选择 Gin + Fx

Gin

这个没什么花哨理由:成熟、稳定、生态好,用起来顺手。

Fx

Fx 一开始我也只是当作 DI 工具在用,但真正用到工程里之后发现:

它更像是一个项目启动调度器

  • 统一管理组件初始化
  • 统一管理启动流程
  • 统一管理生命周期
  • 自动处理依赖顺序

慢慢发现,它更适合做"工程结构工具",而不只是"依赖注入工具"。


四、现在的工程结构

目前项目结构是这样组织的:

csharp 复制代码
cmd/
  app/                 # 应用启动入口
internal/
  config/              # 配置管理
  modules/             # 业务模块(user、post、auth 等)
  router/              # 路由层
  middleware/          # HTTP 中间件(鉴权、日志、跨域等)
  dto/                 # 数据传输对象(请求/响应模型)
pkg/
  logger/              # 日志模块
  postgres/            # PostgreSQL 客户端
  redis/               # Redis 客户端
  jwt/                 # JWT 工具库
  oauth2/              # OAuth2(GitHub / Google 登录)
ent/
  schema/              # Ent ORM 模型定义
  migrate/             # 数据库迁移文件
docs/                  # Swagger 文档

我现在的核心习惯是:

  • 业务域拆模块,而不是按技术拆层
  • router 只做路由绑定
  • modules 只做业务逻辑
  • middleware 独立
  • dto 单独管理输入输出模型
  • pkg 放通用基础设施能力

模块结构保持类似 NestJS 的风格:

go 复制代码
modules/user
  user.controller.go
  user.service.go
  user.repository.go
  user.module.go

这种结构的好处很直接:

找功能 = 找模块,不是翻目录
user.controller.go 这样的文件命名方式应该是不符合 Go 的开发规范的,但是目前好像也没遇到什么问题,所以...😊


五、Fx 在工程里的真实作用

现在在这个项目里,Fx 更像是:

一个"工程启动调度器"

它帮我解决的是:

  • 谁先初始化
  • 谁依赖谁
  • 谁什么时候启动
  • 谁什么时候关闭

而不是"我怎么 new 一个对象"。

gin-app 里,Fx 的使用方式也比较偏工程化,而不是零散注入。

1. 启动入口统一调度

整个系统的启动入口集中在 internal/app/app.goRun() 方法中,这里不是简单地 main() + 一堆初始化函数调用,而是通过 fx.New() 作为系统总调度中心

go 复制代码
fx.New(
  fx.Supply(...),
  fx.Provide(
      NewLogger,
      NewPostgresDB,
      NewRedis,
      NewOauth2Client,
      ...
  ),
  modules.APIModule,
  fx.Invoke(startHTTPServer),
).Run()

这里的结构非常清晰:

(1)fx.Supply ------ 外部配置注入

go 复制代码
fx.Supply(
  fx.Annotate(cfg, fx.As(new(config.ConfigInterface))),
)

配置对象作为"外部输入"进入系统容器,而不是在内部到处读取配置文件。

(2)fx.Provide ------ 基础设施统一注册

包括:

  • Logger
  • Postgres
  • Redis
  • JWT
  • OAuth2(Github / Google)
  • Gin Router
  • API Router
  • OpenAPI Router
  • HTTP Server

全部集中注册在一个地方,形成:

基础设施层统一初始化中心

例如数据库注册:

go 复制代码
fx.Annotate(
  func(cfg config.ConfigInterface, logger logger.Interface) (*postgres.Postgres, error) {
    pg, err := postgres.New(...)
    ...
    return pg, nil
  },
  fx.OnStop(func(logger logger.Interface, pg *postgres.Postgres) error {
    pg.Close()
    return nil
  }),
)

同时把生命周期管理也绑定在注册阶段完成。

(3)modules.APIModule ------ 业务系统整体挂载

复制代码
modules.APIModule,

业务模块不是散落注册,而是作为一个完整模块树挂载进系统容器。

(4)fx.Invoke(startHTTPServer) ------ 启动行为声明

go 复制代码
fx.Invoke(startHTTPServer)

系统"启动做什么事"通过声明式绑定:

  • 不在 main 里写启动逻辑
  • 不在构造函数里启动服务
  • 不靠 init() 隐式行为

而是通过 Invoke 显式声明系统启动动作。

配合 Lifecycle

php 复制代码
lc.Append(fx.Hook{
  OnStart: ...
  OnStop:  ...
})

形成完整的:

构建期(Provide) → 装配期(Module) → 启动期(Invoke) → 生命周期管理(Lifecycle)

这使整个系统从"过程式启动"变成了声明式启动模型

不是"我在 main 里手动控制顺序",而是:

系统结构决定启动顺序
依赖关系决定初始化顺序

Fx 在这里更像是一个:工程启动调度框架,而不仅是依赖注入工具。

2. 模块化注册方式(业务模块即路由单元)

在这个项目里,模块不仅是业务单元,同时也是路由注册单元

每个业务模块都有自己的 module.go,不仅负责依赖注册,也负责路由挂载,例如 user.module.go

go 复制代码
var Module = fx.Module(
  "user",

  fx.Provide(
    // user repository
    fx.Annotate(
      NewUserRepository,
      fx.As(new(UserRepository)),
    ),
    // user service
    fx.Annotate(
      NewUserService,
      fx.As(new(UserService)),
    ),
    // user controller
    NewUserController,
  ),

  // register router
  fx.Invoke(func(
    router api_router.APIRouterParams,
    userController *UserController,
  ) {
    // public routers
    {
      userGroup := router.Public.Group("/users")
      userGroup.POST("", userController.CreateOne)
    }

    // private routers
    {
      userGroup := router.Private.Group("/users")
      userGroup.GET(":id", userController.GetById)
    }
  }),
)

这里有一个非常重要的设计点:

路由注册跟着业务模块走,而不是集中在 router 目录统一管理。

也就是说:

  • 一个模块 = 一组业务能力
  • 一个模块 = 一组路由定义
  • 一个模块 = 一组 Controller + Service + Repository

模块本身就是一个完整业务闭环单元

这样带来的效果非常直观:

  • 新增功能 = 新增模块
  • 删除功能 = 删除模块
  • 查功能 = 查模块

而不是在 router.go 里翻几十个路由定义文件。

从工程体验上来说,这种方式非常接近 NestJS 的 Module + Controller 组织模式,只不过是用 Fx 的 Module 机制实现。


3. 路由系统的初始化解耦设计

internal/router 目录只负责一件事:Gin 本身的初始化与中间件装配,不关心任何业务路由。

Gin 初始化(系统层)

go 复制代码
func NewRouter(
  cfg config.ConfigInterface,
  logger logger.Interface,
) *gin.Engine {
  app := gin.Default()

  // middlewares
  app.Use(requestid.New())
  app.Use(cors.New(...))
  app.Use(ginzap.GinzapWithConfig(...))
  app.Use(ginzap.RecoveryWithZap(...))
  app.Use(timeout.Timeout(...))
  app.Use(middleware.ErrorHandler(logger))

  // Swagger
  if cfg.Swagger().Enabled {
    app.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
  }

  return app
}

这一层只做系统级能力:

  • 中间件装配
  • 日志系统
  • 跨域
  • 超时控制
  • Swagger

完全不出现任何业务路由。

API Router 抽象层

通过 Fx 构造 API Router:

go 复制代码
type APIRouter struct {
  fx.Out

  Public  *gin.RouterGroup `name:"api:public"`
  Private *gin.RouterGroup `name:"api:private"`
}

func NewAPIRouter(
  cfg config.ConfigInterface,
  jwt jwt.JWTManagerInterface,
  app *gin.Engine,
) APIRouter {
  public := app.Group("/api/v1")
  private := public.Group("/")

  private.Use(middleware.APIAuthHandler(jwt, cfg.HTTP().CookieName))

  return APIRouter{
    Public:  public,
    Private: private,
  }
}

通过 fx.Out + name tag

  • 系统级 Router 负责分组
  • 模块级 Router 只消费分组

业务模块只关心:

go 复制代码
router.Public
router.Private

而不关心:

  • Gin Engine 从哪里来
  • 中间件怎么装
  • JWT 鉴权怎么挂

形成清晰分层:

系统层:Router 初始化 + 中间件 + 分组策略

模块层:消费 RouterGroup,注册业务路由

这让整个系统的路由组织方式变成:

系统提供"路由能力"

模块声明"路由行为"

而不是传统的:

一个 router 目录管理全世界路由

每个业务模块都有自己的 module.go,类似:

go 复制代码
var Module = fx.Module(
  "user",
  fx.Provide(
    NewUserService,
    NewUserRepository,
    NewUserController,
  ),
)

模块只关心:

  • 自己提供什么能力
  • 自己依赖什么能力

不关心:

  • 谁来调用我
  • 谁先启动系统

3. 路由注册解耦

路由层通过 fx.Invoke 统一注册:

go 复制代码
fx.Invoke(RegisterRouter)

Router 层只依赖 Controller,不反向依赖业务模块实现,形成清晰依赖方向。

4. 生命周期统一管理

通过 Fx 的 Lifecycle 机制统一管理服务生命周期:

go 复制代码
lc.Append(fx.Hook{
  OnStart: func(ctx context.Context) error {
    return server.Start()
  },
  OnStop: func(ctx context.Context) error {
    return server.Stop()
  },
})

这让:

  • 服务启动顺序可控
  • 资源初始化有序
  • 支持优雅关闭
  • 系统状态统一管理

在实际使用中,Fx 带来的最大价值不是"省几行代码",而是:

把系统结构从"手动组织"变成"声明式组织"

系统从"人脑调度"变成"系统调度"。


六、模块化的真实体验

模块化之后,一个很直观的感受是:

  • 模块边界清晰
  • 改代码不容易误伤其他模块
  • 重构更安全
  • 依赖关系更清楚

模块不再是"文件分类",而是业务单元


七、这套结构带来的真实变化

用这套结构写项目之后,最大的变化不是"写得更快",而是:

  • 项目结构更稳
  • 维护压力更小
  • 重构成本更低
  • 新人更容易理解项目
  • 心理负担更小(笑)

八、写在最后

gin-app 对我来说,不是一个"作品",而更像是:

一套在真实项目中不断试错、调整、演化出来的工程结构

它不是标准答案,也不是最佳实践,只是一个:

当前阶段让我用起来最舒服的一种 Go 工程组织方式

如果你也在做 Go 项目工程化,可能你也会遇到类似的问题。

这篇文章只是一次个人实践分享,希望能提供一些思路参考,而不是范式标准。


项目地址

GitHub:
github.com/1111mp/gin-...


如果你也在做 Go 工程化探索,欢迎交流实践经验 🙌

相关推荐
We་ct4 小时前
LeetCode 56. 合并区间:区间重叠问题的核心解法与代码解析
前端·算法·leetcode·typescript
毅炼5 小时前
Java 基础常见问题总结(4)
java·后端
张3蜂5 小时前
深入理解 Python 的 frozenset:为什么要有“不可变集合”?
前端·python·spring
无小道5 小时前
Qt——事件简单介绍
开发语言·前端·qt
广州华水科技5 小时前
GNSS与单北斗变形监测技术的应用现状分析与未来发展方向
前端
想用offer打牌5 小时前
MCP (Model Context Protocol) 技术理解 - 第一篇
后端·aigc·mcp
code_YuJun5 小时前
corepack 作用
前端
千寻girling5 小时前
Koa.js 教程 | 一份不可多得的 Node.js 的 Web 框架 Koa.js 教程
前端·后端·面试
全栈前端老曹5 小时前
【MongoDB】Node.js 集成 —— Mongoose ORM、Schema 设计、Model 操作
前端·javascript·数据库·mongodb·node.js·nosql·全栈