3. 模型定义基础:结构体标签、表名映射与字段类型
在 GORM 中,模型(Model)是 Go 结构体与数据库表之间的桥梁。通过结构体标签(Tag)和约定方法,开发者可以精确控制表名、字段类型、主键、自增 ID 以及时间字段的自动管理。本章将系统讲解这些核心映射规则,帮助你写出清晰、可维护的模型定义。
核心原则: GORM 遵循“约定优于配置”的设计哲学。只要遵循默认命名规则,大部分映射可以自动完成;当需要偏离默认行为时,通过标签或接口方法显式声明。
3.1 结构体标签(gorm 标签)
GORM 使用 gorm 标签来定义字段级别的映射规则。标签格式为 gorm:"key1:value1;key2:value2",多个键值对之间用分号分隔。
| 标签键 | 说明 | 示例值 |
|---|---|---|
column |
指定数据库列名 | column:user_name |
type |
指定数据库字段类型 | type:varchar(100) |
size |
字段大小/长度(varchar 等类型) | size:255 |
primaryKey |
标记为主键 | primaryKey |
autoIncrement |
启用自增 | autoIncrement |
not null |
非空约束 | not null |
unique |
唯一约束 | unique |
default |
默认值 | default:0 |
comment |
字段注释 | comment:用户昵称 |
index |
索引(可指定索引名) | index:idx_name |
embedded |
嵌入结构体 | embedded |
- |
忽略该字段(不映射到数据库) | - |
最佳实践: 使用
column 标签显式指定列名可以避免因 Go 字段名重构导致数据库列名意外变化。对于生产环境模型,建议始终为关键字段添加 column 标签。
3.2 表名映射
GORM 默认将结构体名转换为蛇形复数形式作为表名。例如 UserInfo 对应 user_infos。当需要自定义表名时,有三种方式:
- 实现
Tabler接口: 定义TableName() string方法,返回自定义表名。 - 使用
Table()方法: 在查询时动态指定表名。 - 全局表名前缀: 通过
gorm.Config的NamingStrategy设置前缀。
// 方式一:实现 Tabler 接口
type User struct {
ID uint
Name string
}
func (User) TableName() string {
return "t_user" // 自定义表名
}
// 方式二:动态指定表名
db.Table("t_user").Where("name = ?", "张三").Find(&users)
// 方式三:全局前缀
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "t_", // 所有表名自动添加 t_ 前缀
},
})
注意: 如果同时使用
TableName() 方法和 Table() 方法,Table() 的优先级更高。在同一个项目中,建议统一使用一种方式管理表名,避免混淆。
3.3 字段类型映射
GORM 自动将 Go 数据类型映射到数据库字段类型。下表列出常见映射关系:
| Go 类型 | MySQL 类型 | PostgreSQL 类型 | SQLite 类型 |
|---|---|---|---|
int, uint |
bigint |
bigint |
integer |
int8, uint8 |
tinyint |
smallint |
integer |
int16, uint16 |
smallint |
smallint |
integer |
int32, uint32 |
int |
integer |
integer |
int64, uint64 |
bigint |
bigint |
integer |
float32 |
float |
real |
real |
float64 |
double |
double precision |
real |
string |
longtext |
text |
text |
bool |
tinyint(1) |
boolean |
numeric |
time.Time |
datetime(3) |
timestamp with time zone |
datetime |
[]byte |
longblob |
bytea |
blob |
当需要精确控制字段类型时,使用 type 标签覆盖默认映射:
type Product struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"type:varchar(200);not null;comment:商品名称"`
Price float64 `gorm:"type:decimal(10,2);default:0.00"`
Description string `gorm:"type:text"`
Status int8 `gorm:"type:tinyint(1);default:1;comment:状态 1-上架 0-下架"`
}
3.4 主键与自增 ID
GORM 默认将名为 ID 的字段作为主键,并自动启用自增。如果字段名不是 ID,需要显式使用 primaryKey 标签。
// 默认主键(自动识别)
type User struct {
ID uint // 自动成为主键并自增
Name string
}
// 自定义主键字段名
type Order struct {
OrderID uint `gorm:"primaryKey;autoIncrement;column:order_id"`
Amount float64
}
// 复合主键
type OrderItem struct {
OrderID uint `gorm:"primaryKey"`
ProductID uint `gorm:"primaryKey"`
Quantity int
}
自增 ID 注意事项:
- 自增字段类型必须是整数类型(
uint、int、uint64等)。 - 使用
autoIncrement标签显式声明自增,避免依赖数据库默认行为。 - 复合主键不支持自增,每个主键字段需要手动赋值。
- 在分布式场景下,建议使用 UUID 或雪花算法 ID 替代自增主键。
3.5 时间字段自动管理
GORM 通过约定字段名自动管理创建时间和更新时间。当结构体包含 CreatedAt 和 UpdatedAt 字段时,GORM 会在插入和更新时自动填充当前时间。
type User struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"type:varchar(100);not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` // 创建时自动填充
UpdatedAt time.Time `gorm:"autoUpdateTime"` // 更新时自动填充
DeletedAt gorm.DeletedAt `gorm:"index"` // 软删除字段
}
时间字段的自动管理支持三种模式:
| 标签 | 行为 | 适用场景 |
|---|---|---|
autoCreateTime |
创建记录时自动设置当前时间,后续不再修改 | 记录创建时间 |
autoUpdateTime |
创建和更新记录时自动设置为当前时间 | 记录最后修改时间 |
autoCreateTime:milli |
创建时自动填充毫秒级时间戳 | 需要高精度时间戳的场景 |
autoUpdateTime:nano |
更新时自动填充纳秒级时间戳 | 需要极高精度的时间记录 |
时间精度选择: 默认使用
time.Time 类型时,数据库存储精度为秒级。如果业务需要毫秒或纳秒精度,使用 autoCreateTime:milli 或 autoUpdateTime:nano 标签,并将字段类型改为 uint 或 int64。
3.6 完整模型示例
综合以上知识点,下面是一个生产级别的模型定义示例:
package model
import (
"time"
"gorm.io/gorm"
)
// User 用户模型
type User struct {
ID uint `gorm:"primaryKey;autoIncrement;column:id"`
Username string `gorm:"type:varchar(50);not null;unique;index:idx_username;comment:用户名"`
Email string `gorm:"type:varchar(100);not null;unique;comment:邮箱"`
Age int8 `gorm:"type:tinyint(3);default:0;comment:年龄"`
Avatar string `gorm:"type:varchar(255);default:'';comment:头像URL"`
Status int8 `gorm:"type:tinyint(1);default:1;comment:状态 1-正常 0-禁用"`
CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间"`
}
// TableName 自定义表名
func (User) TableName() string {
return "t_user"
}
模型设计建议:
- 为每个字段添加
comment标签,便于数据库维护和文档生成。 - 为查询频繁的字段添加
index标签,提升查询性能。 - 使用
gorm.DeletedAt实现软删除,避免物理删除数据。 - 时间字段统一使用
time.Time类型,保持代码可读性。
3.7 常见错误与避坑指南
| 错误场景 | 错误原因 | 解决方案 |
|---|---|---|
| 字段未映射到数据库 | 字段名首字母小写(未导出) | 确保字段名首字母大写 |
| 自增 ID 不生效 | 字段类型不是整数或未加 autoIncrement 标签 |
使用 uint 类型并添加 autoIncrement 标签 |
| 时间字段未自动填充 | 字段名不是 CreatedAt/UpdatedAt 或缺少标签 |
使用约定字段名或添加 autoCreateTime/autoUpdateTime 标签 |
| 表名与预期不符 | 未实现 TableName() 方法且默认复数规则不符合需求 |
实现 Tabler 接口或配置 NamingStrategy |
| 字段类型不匹配 | Go 类型与数据库类型不兼容 | 使用 type 标签显式指定数据库类型 |
重要提醒: 模型定义是数据库迁移的基础。在定义模型时,务必考虑字段的约束(非空、唯一、默认值)和索引策略,这些将直接影响数据库的性能和数据完整性。建议在模型定义阶段就与 DBA 或后端架构师对齐规范。