深色模式
Goa 接入 GORM
概述
Goa 和 GORM 解决的不是同一件事。Goa 负责接口契约、传输层和类型生成,GORM 负责数据持久化。中小项目把这两者接在一起时,最关键的不是“能不能连上数据库”,而是别把接口类型、数据库模型和业务规则揉成一层。
如果边界不清,很容易出现这些问题:生成类型被拿去打数据库标签,GORM 模型直接暴露到 API 响应里,或者一个 service 方法同时处理参数校验、事务、SQL 和 JSON 结构。代码短期能跑,后期基本难维护。
先把职责分开
比较稳的分工可以是这样:
- Goa:定义
Payload、Result、Error和 HTTP 映射 - Service:编排业务流程
- Repository:封装数据库读写
- GORM Model:描述表结构和持久化字段
也就是说,Goa 生成的类型和 GORM 模型通常不是同一个对象。
一个适合中小项目的目录
text
internal/
├── user/
│ ├── service.go
│ ├── repo.go
│ ├── model.go
│ └── mapper.go
└── platform/
└── db.go这套结构已经够覆盖大多数中小型后台:
service.go:面向 Goa 接口,实现业务流程repo.go:封装数据库访问model.go:定义数据库模型mapper.go:做模型和结果类型转换
如果项目更小,repo.go 和 service.go 也可以先放一个文件里,但“接口类型”和“数据库模型”最好还是分开。
不要直接拿生成类型当数据库模型
例如 Goa 可能生成这样的结果类型:
go
type User struct {
ID uint64
Name string
Email string
}而数据库模型往往还会多出这些字段:
go
type UserModel struct {
ID uint64 `gorm:"primaryKey"`
Name string
Email string `gorm:"uniqueIndex"`
Password string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt
}两者服务的对象完全不同:
- Goa 结果类型是对外响应
- GORM 模型是对内存储
如果强行合并,后面很容易出现字段泄露、接口结构被数据库拖着走、或者数据库改字段后接口一起被迫重排的问题。
一个更实用的接法
先定义仓储接口:
go
package user
import "context"
type Repository interface {
Create(ctx context.Context, m *UserModel) error
FindByID(ctx context.Context, id uint64) (*UserModel, error)
}再实现 Goa service:
go
package user
import (
"context"
genuser "example.com/blogapi/gen/user"
)
type Service struct {
repo Repository
}
func NewService(repo Repository) *Service {
return &Service{repo: repo}
}
func (s *Service) Create(ctx context.Context, p *genuser.CreatePayload) (*genuser.User, error) {
model := &UserModel{
Name: p.Name,
Email: p.Email,
}
if err := s.repo.Create(ctx, model); err != nil {
return nil, err
}
return toResult(model), nil
}
func toResult(m *UserModel) *genuser.User {
return &genuser.User{
ID: m.ID,
Name: m.Name,
Email: m.Email,
}
}这种写法看起来多了一层转换,但它换来的是更清楚的边界。对中小项目来说,这个成本通常很值。
事务放在哪一层
事务通常放在应用服务层或专门的事务封装里,而不是放进 Goa DSL,也不是散落在每个 handler 里。
一个常见做法是:
- service 方法决定是否开启事务
- repository 接收事务上下文或
*gorm.DB - Goa 接口层只负责把请求交给 service
这样事务边界和业务动作是一致的,不会被 HTTP 细节带偏。
数据库连接和迁移别塞进 Goa
数据库连接初始化、连接池参数、迁移脚本、种子数据,这些都属于平台和部署层,应该放在 cmd/ 或 internal/platform/ 里,不属于 design/。
Goa 只需要知道接口长什么样,不需要知道 PostgreSQL DSN、连接池大小或者表是怎么迁移出来的。
中小项目里更稳的实践
参数校验优先放在 Goa 边界
字段缺失、格式不对、必填项没传,这类输入校验优先交给 Goa 的 Payload 约束。业务层更应该关心“这个邮箱是否已存在”“这个用户状态能不能操作”。
数据库错误尽量在 service 层翻译
例如唯一索引冲突、记录不存在这类错误,最好在 service 层翻译成业务错误,再交给 Goa 错误映射,而不是把数据库驱动的原始错误直接往上传。
WithContext 直接接 Goa 的 ctx
Goa 生成的方法签名自带 context.Context,传给 GORM 的 WithContext 就可以了。请求超时、取消和链路信息也能顺着带过去。
