模型层是请求数据载体与业务契约,非ORM映射;应为每个接口定义独立扁平的Input/Output结构体,字段小写+显式JSON tag,禁用数据库标签和复用,用Bind()校验,按接口边界分目录而非数据库表。

模型层不是 ORM,别往 struct 里塞数据库标签
很多人一写 struct 就条件反射加 gorm.Model 或 sqlx.DB 相关字段,但 Iris 的模型层(MVC 中的 model)本质是「请求数据载体 + 业务逻辑输入输出契约」,和数据库映射无关。它不参与持久化,也不该依赖任何 DB 驱动。如果你在 model 里看到 gorm:"primaryKey" 或 json:",omitempty" 混用、甚至嵌套 sql.NullString,说明职责已经错位。
正确做法是:为每个 HTTP 接口定义独立、扁平、只含必要字段的 struct,字段名用 Go 风格小写,JSON tag 显式声明(避免默认驼峰转换出错):
type CreateUserInput struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
type UserOutput struct {
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
- 输入结构体(
CreateUserInput)只接收客户端传入的字段,不做额外嵌套或指针字段(除非明确需要空值语义) - 输出结构体(
UserOutput)只暴露应返回的字段,时间字段显式用time.Time而非字符串,由 Iris 自动序列化为 RFC3339 格式 - 绝不复用同一个 struct 既做输入又做输出——字段可选性、校验规则、序列化行为往往不同
控制器里怎么用 model?别直接 new(),用 Bind() 做校验兜底
Iris 提供 ctx.ReadJSON() 和 ctx.ReadForm(),但更推荐用 ctx.Bind() 统一入口。它会自动根据 Content-Type 选择解析方式,并触发结构体字段上的验证标签(需配合 github.com/go-playground/validator/v10)。
示例中若没做绑定校验,前端传 {"email": "invalid"},后端可能静默接受并走到 DB 层才报错:
func (c *UserController) Post() string {
var input CreateUserInput
if err := c.Ctx.Bind(&input); err != nil {
c.Ctx.StatusCode(400)
return "invalid input: " + err.Error()
}
// 后续业务逻辑...
}
-
Bind()默认启用validate标签,比如Name string `json:"name" validate:"required,min=2,max=20"` - 不要手动调用
json.Unmarshal()—— 会绕过验证,也丢失对form-data/urlencoded的兼容 - 如果接口同时支持 JSON 和表单提交,
Bind()是唯一可靠选择;ReadJSON()强制要求application/json,否则 panic
模型层要不要分目录?按接口边界切,别按数据库表切
常见错误是建一个 models/ 目录,然后把所有 DB 表结构体全扔进去,再让 controller import 整个包。这会导致模型层和存储层强耦合,且无法表达「这个 API 需要哪几个字段」的语义。
推荐结构:
handlers/
user/
create.go // 定义 CreateUserInput / CreateUserOutput
list.go // 定义 ListUsersInput / UserSummaryOutput
profile.go // 定义 UserProfileOutput(字段比 list 更多)
- 每个 handler 文件内定义自己专属的
Input/Output,不跨文件复用 - 字段命名以接口视角为准,比如
UserSummaryOutput可能只有ID,Name,AvatarURL,而不用照搬 DB 表的updated_at或is_deleted - 避免
models.User这种全局 struct —— 它天然暗示「所有地方都用同一套字段」,实际根本不是
嵌套结构体怎么处理?用内联(anonymous struct)或组合,别深层嵌套
当 API 返回带关联数据的对象(如用户+其头像信息),容易写出三层嵌套的 struct:
// ❌ 不推荐:层级深、难测试、JSON key 路径长
type UserWithAvatar struct {
User struct {
ID uint `json:"id"`
Name string `json:"name"`
Profile struct {
AvatarURL string `json:"avatar_url"`
} `json:"profile"`
} `json:"user"`
}
改成扁平组合更清晰:
// ✅ 推荐:字段直出,语义明确,易 mock
type UserWithAvatar struct {
ID uint `json:"id"`
Name string `json:"name"`
AvatarURL string `json:"avatar_url"`
}
- 如果多个接口都需要「用户+头像」,就单独提一个
UserWithAvatar,而不是从User嵌套拼装 - 嵌套匿名 struct 仅适用于极简场景(如只用一次的临时过滤参数),长期存在请定义具名 struct
- 深层嵌套会让 Swagger 文档生成混乱,Iris 的
ctx.JSON()也不会报错,但前端解析成本陡增
CreatedAt 字段,不是模型层去赋值的,而是 service 层或 repository 层决定的。模型只是容器,装什么、谁来装、装完给谁看——这些决策点必须外移。


















