Fiber 的“控制器分层”是开发者为解耦职责而采用的代码组织模式,非框架内置概念;它通过 Group 划分路由前缀实现路由层与控制器层物理隔离,控制器仅负责解析请求、调用服务、格式化响应,服务层须完全 unaware of Fiber,依赖通过构造函数注入,Ctx 仅作为方法参数传入以保障可测试性与独立演化能力。

什么是 Fiber 的“控制器分层”?
它不是 Fiber 框架内置的概念,而是开发者为组织业务逻辑、解耦职责而采用的代码结构模式。Fiber 本身只提供 App 和 Group 两类路由注册入口,不强制 MVC 或分层约定。所谓“控制器分层”,本质是把原本平铺在 app.Get("/user", handler) 中的逻辑,按关注点拆到不同层级:路由层(绑定路径)、控制器层(协调输入/输出)、服务层(核心业务)、领域模型层(数据结构)。
用 Group 实现路由层与控制器层的物理隔离
最直接可行的方式是利用 Group 划分路由前缀,并将控制器函数统一挂载到对应 Group 上,避免所有 handler 都写在根 App 实例里。
-
Group不是中间件——只有显式传入handlers参数时才被标记为中间件;不传则纯作路径分组容器 - 每个 Group 应对应一个业务域(如
/api/v1/user),其内部 handler 只负责解析请求、调用控制器方法、返回响应 - 控制器函数建议定义为独立函数或结构体方法,**不要直接在 Group 内联写业务逻辑**
示例:
userGroup := app.Group("/api/v1/user")
userGroup.Get("/", func(c *fiber.Ctx) error {
return userController.List(c) // 调用控制器方法
})
userGroup.Post("/", func(c *fiber.Ctx) error {
return userController.Create(c)
})
控制器层如何与服务层解耦?
Fiber 的 *fiber.Ctx 是请求上下文,但不应成为服务层的依赖源。控制器层应只做三件事:校验输入、调用服务、格式化输出。服务层必须完全 unaware of Fiber。
- 服务函数签名应为纯 Go 函数,参数只接收结构体(如
user.CreateInput)或基础类型,返回值明确(user.User,error) - 控制器内禁止出现
c.Query(...)或c.Status(...)等 Fiber 特有调用出现在服务层 - 若需共享配置或 DB 实例,通过构造函数注入控制器结构体,而非从全局容器(如
app.Get("db"))获取
错误示范:func CreateUser(c *fiber.Ctx) error { db := app.Get("db").(*sql.DB); ... } → 违反可测试性与分层边界
为什么不能靠依赖注入容器自动绑定控制器?
Fiber 本身无 DI 容器,第三方方案(如 uber/fx 或 goava/di)虽可集成,但在分层实践中容易模糊控制流。尤其当控制器方法需要访问 *fiber.Ctx 时,DI 容器无法安全注入运行时对象。
-
*fiber.Ctx生命周期仅限单次 HTTP 请求,不可复用或缓存 - 若强行让 DI 容器管理控制器实例并注入
Ctx,会导致每次请求都重建控制器,失去结构体方法封装的意义 - 更稳妥的做法是:控制器结构体只持服务依赖,
Ctx作为方法参数传入,保持无状态
推荐写法:
type UserController struct {
userService UserService
}
func (uc *UserController) List(c *fiber.Ctx) error {
users, err := uc.userService.FindAll()
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(users)
}
容易忽略的关键点
分层不是加目录和文件夹就能成立的。真正的分层失效往往发生在控制器和服务的边界上:比如服务层开始调用 c.Locals 存取上下文变量,或控制器里直接执行 SQL 查询。一旦出现这类混用,后续想抽离为 CLI 命令、gRPC 接口或单元测试都会卡住。分层的价值不在结构漂亮,而在让每层能独立演化——例如把 UserService 替换为远程 gRPC client 时,控制器代码一行都不用改。


















