fiber 项目应避免硬套传统 mvc,而需按 handler→service→repository→model 单向依赖分层,各层职责分明、不暴露框架类型,通过构造函数注入依赖实现解耦。

Fiber 本身不内置 MVC 概念,硬套传统 MVC(比如把 controller、model、view 当成三个强耦合包)会迅速掉进循环导入、职责混乱、测试困难的坑里。Go 的编译模型和包管理机制决定了:MVC 是一种组织意图,不是框架契约。
为什么直接按 Spring Boot 那套建 controller/service/dao 目录会出问题
常见错误现象:import cycle not allowed 报错;service 层调用 handler 里的响应工具;model 包里出现 fiber.Ctx 或 json.NewEncoder —— 这些都不是 Fiber 的锅,而是目录结构没守住边界。
-
controller目录里写数据库操作或业务判断 → 实际是service和repository的活 -
model包里定义User.Create(ctx *fiber.Ctx)→ 数据结构不该感知 HTTP 上下文 - 所有 handler 函数都依赖全局
db *gorm.DB变量 → 无法 mock,单元测试写不下去 - 用
fiber.Group()模拟“Controller 类”,再塞一堆闭包中间件 → 最终变成状态难追踪的函数堆砌
Fiber 项目中真正该有的分层与依赖流向
核心原则:依赖只能单向向下,handler → service → repository → model,反向绝不允许。每层只暴露接口或纯函数,不暴露框架类型。
-
handler:只做三件事 —— 解析参数(c.Params、c.Body)、调用service方法、写响应(c.JSON或c.Status().SendString) -
service:接收已校验的输入(如user_service.CreateUser(req CreateUserReq)),组合多个repository调用,处理事务和业务规则 -
repository:只封装数据访问,返回model.User或error,不碰 HTTP、不封装响应逻辑 -
model:纯 Go 结构体 + 字段 tag(如json:"id"、gorm:"primaryKey"),零方法、零依赖
示例路径结构:
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
internal/
├── handler/
│ └── user_handler.go // func CreateUser(c *fiber.Ctx)
├── service/
│ └── user_service.go // func CreateUser(req CreateUserReq) (User, error)
├── repository/
│ └── user_repo.go // func Create(u model.User) error
└── model/
└── user.go // type User struct { ID int `json:"id"` }
如何让 Fiber handler 与 service 层解耦
关键不是“怎么注册路由”,而是“怎么把依赖传进去”。别用全局变量,用构造函数注入。
- 在
main.go初始化时,先创建repo := &user_repo.GormUserRepo{DB: db},再传给service := &user_service.UserService{Repo: repo},最后注册 handler:app.Post("/users", user_handler.MakeCreateUserHandler(service)) -
MakeCreateUserHandler是一个工厂函数,返回func(*fiber.Ctx),它把service闭包进去,避免 handler 直接 import service 包 - 这样
handler包就不用 importservice,只依赖func类型,方便替换和测试
简短示意:
func MakeCreateUserHandler(svc UserService) fiber.Handler {
return func(c *fiber.Ctx) error {
var req CreateUserReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
u, err := svc.CreateUser(req)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(fiber.StatusCreated).JSON(u)
}
}
容易被忽略的细节:错误处理与响应包装不能放在 model 或 repository
很多团队把 ErrNotFound 定义在 model 包里,然后在 repository 中 return model.ErrNotFound,接着在 handler 里 switch 错误类型转 HTTP 状态码 —— 这看似优雅,实则把表现层逻辑泄漏到了数据层。
- 错误应由
service层统一归一化(比如返回自定义AppError类型,含Code字段) -
handler根据AppError.Code映射状态码,不关心底层是 GORM 还是 Redis -
repository层只返回原始 error(如gorm.ErrRecordNotFound),由service转换,不越界
这种切分让换数据库、加缓存、改协议(比如未来支持 GraphQL)时,只需动 handler 和 service,repository 和 model 完全不动。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










