项目布局规范:cmd/internal/handler/service/dao 分层与依赖方向

Go 官方推荐的布局是 cmd/ 放入口、internal/ 放私有代码;Web 项目再按职责细分 handler(HTTP 层)、service(业务层)、dao(数据层)。分层价值不在目录好看,而在依赖方向清晰、可替换、可测试。

1. 目录结构

myapp/
├── cmd/
│   └── server/main.go        # 入口:装配配置、路由、启动
├── internal/
│   ├── config/               # 读取环境变量/配置文件
│   ├── dao/                  # 数据访问:GORM 查询封装
│   ├── model/                # 数据模型结构体
│   ├── service/              # 业务逻辑:校验、事务编排
│   ├── handler/              # HTTP:绑定参数、调 service、组装响应
│   ├── middleware/           # 鉴权、限流、日志等中间件
│   └── router/               # 路由注册(装配 handler 与 middleware)
├── go.mod
└── go.sum

2. 依赖方向:只允许向下调

一条铁律:handler → service → dao → model,禁止反向或跨层调用:

// dao:只碰 GORM,返回数据或错误,不写 HTTP
type UserDAO struct{ db *gorm.DB }

func (d *UserDAO) ByID(id uint) (*model.User, error) {
    var u model.User
    err := d.db.First(&u, id).Error
    return &u, err
}

// service:编排业务,依赖接口而非具体实现,方便测试注入
type UserSvc struct{ users *UserDAO }

func (s *UserSvc) Get(id uint) (*model.User, error) {
    return s.users.ByID(id)
}

// handler:绑定参数 → 调 service → 输出 JSON
func (h *UserHandler) Get(c *gin.Context) {
    id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
    u, err := h.svc.Get(uint(id))
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
        return
    }
    c.JSON(http.StatusOK, u)
}

3. 装配与依赖注入

main 里自底向上组装,再交给 router:

db := dao.MustOpenDB(cfg)              // 1. 数据层
userDAO := &dao.UserDAO{DB: db}
userSvc := &service.UserSvc{DAO: userDAO}
userH := &handler.UserHandler{Svc: userSvc}
r := router.New(userH, cfg)            // 2. 注册路由
r.Run(cfg.Addr)

手工注入足够小项目;工程变大再考虑 wire(google/wire)等生成式注入。

4. 接口放哪、怎么抽象

  • service 依赖 dao 时定义「dao 接口」,实现用编译期断言校验(var _ DaoIface = (*dao.X)(nil))。
  • 单测用 fake 实现接口,service 层测试不依赖真实数据库。
  • 别为了分层而分层:只暴露调用方真正需要的方法,签名尽量收敛。

注意点

  • internal/ 下的包不能被外部项目 import,天然防误用。
  • handler 不写 SQL、service 不写 HTTP 状态码,各层职责单一。
  • 中间件放 middleware/,鉴权逻辑本身属于 service 层能力,中间件只做搬运。
  • 错误逐层向上抛,不吞错,由 handler 决定状态码与错误体。

小结

布局规范解决的是「代码放哪、谁能调谁」。按 cmd/internal/... 组织 + 单向依赖 + 接口注入,项目就能保持可读、可测、可替换。实战章节将按此结构实现完整接口。

笔记加载中…