fiber 项目推荐四层结构:handler/仅处理请求响应,service/封装业务逻辑,repository/专注数据操作,model/定义数据契约;各层单向依赖,避免耦合与测试困难。

main.go 里 grep 十分钟找某个校验逻辑。
为什么不能把所有代码塞进 main.go 或 handler 目录
刚上手 Fiber 的人常把路由、解析、校验、DB 调用全写在 app.Get() 回调里。短期跑得通,但很快遇到这些实际问题:
-
c.Query("page")和c.BodyParser(&req)混在一起,参数校验分散,改个字段类型就得翻三四个文件 - 同一个业务(比如“创建用户”)的密码哈希、发邮件、写日志、更新缓存全挤在 handler 里,单元测试只能 mock 整个
*fiber.Ctx - 想复用“根据 ID 查用户”逻辑到另一个接口(如订单详情),却发现它绑死了
c.Status().JSON(),没法直接调 - 加中间件(如 JWT 鉴权)时,发现 handler 里已经手动写了
if token == nil { ... },和中间件重复又冲突
推荐的四层结构及各层职责边界
参考你知识库中已验证的 internal/ 分层实践,每层只依赖下层,不反向调用:
-
handler/:只做三件事——读请求(c.Params()/c.Query()/c.BodyParser())、调 service 方法、写响应(c.JSON()/c.Status().SendString())。不碰 DB、不处理业务规则、不写 if-else 判断权限 -
service/:放纯业务逻辑。例如CreateUser(req CreateUserReq) (User, error)里调repo.Create()+mail.SendWelcome()+cache.Invalidate()。它不关心 HTTP,只返回结构体或 error -
repository/:只封装数据操作。GORM 的db.Create()、Redis 的client.Set()全在这里。它接收 model 实例,返回 error 或原始数据(如int64、[]byte),不返回*fiber.Ctx相关东西 -
model/:定义数据契约。struct 字段带json:tag、GORM 的gorm:tag,但不包含方法。别在这里写func (u User) FullName() string—— 那是 service 层的事
常见结构误用与修正方式
真实项目里,下面这些“看似省事”的做法,后续维护成本极高:
- 在
handler/里直接 new GORM DB 实例:db, _ := gorm.Open(...)→ 应该由repository/接收初始化好的*gorm.DB作为依赖注入 - 把
config.yaml解析逻辑放在handler/里:viper.GetString("redis.addr")→ 配置应提前加载,通过构造函数传给 service 或 repo - 用全局变量存 logger:
var log *zap.Logger→ 改为每个 service 结构体字段持有一个log *zap.Logger,便于不同模块独立配置 level - 在
model/里写Validate() error方法 → 校验规则随业务变化频繁,应抽成独立的validator/包或放在 service 层,model 只负责数据承载
router.go 不是路由注册终点,而是入口协调点
很多人把所有 app.Get() 写在 main.go,导致路由和 handler 强耦合。更清晰的做法是:
- 新建
internal/router/router.go,导出SetupRoutes(app *fiber.App, h *handler.UserHandler) - 在
main.go中只初始化依赖(DB、Redis、Logger),然后 new 各层实例,最后调router.SetupRoutes(app, userHandler) - 这样测试时可轻松替换
userHandler为 mock 实现,而不用动路由注册逻辑
真正容易被忽略的是:handler 层必须保持无状态,所有外部依赖(DB、Cache、Mail)都通过构造函数注入,而不是在方法内现场获取——否则你永远无法对 handler 做可靠单元测试。











