3. 模型定义与标签:GORM 模型结构体定义、字段标签(gorm 标签)、主键、自增、默认值、注释
在 GORM 中,模型(Model)是数据库表与 Go 结构体之间的映射桥梁。一个精心定义的模型,不仅决定了表结构,还通过 gorm 标签 精确控制字段的约束、类型、默认值、索引等元信息。本章将系统讲解模型结构体的定义规范、核心标签的用法,以及主键、自增、默认值和注释的实战配置。
核心原则:GORM 模型定义遵循“约定优于配置”,但通过标签可以覆盖绝大多数默认行为。标签是 GORM 迁移和契约校验的基石。
3.1 模型结构体基础定义
一个标准的 GORM 模型是一个普通的 Go 结构体,通常内嵌 gorm.Model 基类或自定义基类。字段使用 gorm 标签声明数据库列属性。
package model
import (
"time"
"gorm.io/gorm"
)
// User 用户模型,对应数据库表 users
type User struct {
gorm.Model
Name string `gorm:"type:varchar(100);not null;default:'';comment:用户名"`
Email string `gorm:"type:varchar(255);uniqueIndex;not null;comment:邮箱"`
Age int `gorm:"default:0;comment:年龄"`
Birthday time.Time `gorm:"type:date;comment:生日"`
Status int `gorm:"default:1;comment:状态 1-启用 0-禁用"`
DeletedAt gorm.DeletedAt `gorm:"index;comment:软删除时间"`
}
| 字段 | 标签配置 | 说明 |
|---|---|---|
gorm.Model |
内嵌结构体 | 包含 ID、CreatedAt、UpdatedAt、DeletedAt 四个基础字段 |
Name |
type:varchar(100);not null;default:'' |
指定列类型、非空、默认空字符串 |
Email |
uniqueIndex;not null |
唯一索引 + 非空约束 |
Age |
default:0 |
默认值 0 |
Birthday |
type:date |
指定数据库日期类型 |
DeletedAt |
index |
软删除字段,自动添加索引 |
最佳实践:始终为每个字段添加
comment 标签,这会在迁移时生成数据库注释,极大提升团队协作和后期维护效率。
3.2 gorm 标签详解
gorm 标签是模型定义的核心语法,支持丰富的配置项。下表列出最常用的标签及其作用域:
| 标签 | 示例值 | 作用 |
|---|---|---|
column |
column:user_name |
指定数据库列名,默认使用蛇形命名 |
type |
type:varchar(255) |
指定数据库列类型,覆盖 GORM 自动推断 |
size |
size:255 |
列大小/长度,对 varchar 等类型有效 |
primaryKey |
primaryKey |
标记为主键 |
autoIncrement |
autoIncrement |
自增字段 |
unique |
unique |
唯一约束 |
uniqueIndex |
uniqueIndex:idx_email |
唯一索引,可指定索引名 |
index |
index:idx_status |
普通索引,可指定索引名 |
default |
default:0 |
默认值 |
not null |
not null |
非空约束 |
comment |
comment:用户邮箱 |
字段注释,迁移时写入数据库 |
serializer |
serializer:json |
指定序列化方式(json/gob/unixtime 等) |
embedded |
embedded |
嵌入结构体,将字段展开到父表 |
embeddedPrefix |
embeddedPrefix:addr_ |
嵌入字段的前缀 |
注意:标签中的多个配置项使用分号
; 分隔,不要使用空格或逗号。例如:type:varchar(100);not null;default:''。
3.3 主键与自增配置
GORM 默认使用名为 ID 的字段作为主键,并自动设置为自增。但实际项目中经常需要自定义主键字段或复合主键。
3.3.1 默认主键
type Product struct {
ID uint `gorm:"primaryKey;autoIncrement;comment:产品ID"`
Name string `gorm:"type:varchar(200);not null;comment:产品名称"`
Price float64 `gorm:"type:decimal(10,2);default:0.00;comment:价格"`
}
3.3.2 自定义主键字段
type Order struct {
OrderID string `gorm:"primaryKey;type:varchar(32);comment:订单号"`
UserID uint `gorm:"not null;comment:用户ID"`
Amount float64 `gorm:"type:decimal(12,2);comment:订单金额"`
CreatedAt time.Time
}
3.3.3 复合主键
type OrderItem struct {
OrderID string `gorm:"primaryKey;type:varchar(32);comment:订单号"`
ProductID uint `gorm:"primaryKey;comment:产品ID"`
Quantity int `gorm:"default:1;comment:数量"`
Price float64 `gorm:"type:decimal(10,2);comment:单价"`
}
提示:复合主键在迁移时会自动创建联合主键约束。GORM 不支持在复合主键上使用
autoIncrement,自增只能用于单字段主键。
3.4 默认值策略
GORM 的默认值分为两个层面:Go 结构体默认值 和 数据库默认值。两者需要配合使用才能达到预期效果。
| 场景 | Go 默认值 | gorm 标签 | 迁移结果 |
|---|---|---|---|
| 纯数据库默认值 | 不设置 | default:0 |
数据库列有 DEFAULT 0,Go 零值插入时使用默认值 |
| 纯 Go 默认值 | Age: 18 |
不设置 default | 数据库无默认值,Go 创建时自动填充 18 |
| 双重默认值 | Status: 1 |
default:1 |
数据库有 DEFAULT 1,Go 零值也使用 1,保持一致 |
| 零值陷阱 | 不设置 | default:0 |
int 零值 0 会被 GORM 忽略,不会触发数据库默认值 |
零值陷阱详解:对于 int、float 等类型,Go 的零值(0、0.0)与数据库默认值冲突。GORM 在创建记录时,如果字段值为零值,会忽略该字段,导致数据库默认值不生效。解决方案:
1. 使用指针类型
2. 使用
3. 在 Go 结构体中显式设置非零默认值。
1. 使用指针类型
*int,零值为 nil,可触发数据库默认值。2. 使用
sql.NullInt64 等包装类型。3. 在 Go 结构体中显式设置非零默认值。
3.5 字段注释与文档化
注释是模型契约的重要组成部分。GORM 通过 comment 标签在迁移时生成数据库注释,同时建议在结构体字段上方添加 Go 文档注释。
// UserProfile 用户档案表
type UserProfile struct {
gorm.Model
// UserID 关联用户ID,唯一约束
UserID uint `gorm:"uniqueIndex;not null;comment:关联用户ID"`
// Nickname 用户昵称,最长50字符
Nickname string `gorm:"type:varchar(50);default:'';comment:用户昵称"`
// Avatar 头像URL
Avatar string `gorm:"type:varchar(500);default:'';comment:头像URL"`
// Bio 个人简介
Bio string `gorm:"type:text;comment:个人简介"`
}
迁移后,数据库中的注释效果:
-- MySQL 查看表结构
mysql> SHOW FULL COLUMNS FROM user_profiles;
+----------+--------------+-------------------+------+-----+---------+----------------+
| Field | Type | Comment | Null | Key | Default | Extra |
+----------+--------------+-------------------+------+-----+---------+----------------+
| id | bigint(20) | 主键ID | NO | PRI | NULL | auto_increment |
| user_id | bigint(20) | 关联用户ID | NO | UNI | NULL | |
| nickname | varchar(50) | 用户昵称 | YES | | '' | |
| avatar | varchar(500) | 头像URL | YES | | '' | |
| bio | text | 个人简介 | YES | | NULL | |
+----------+--------------+-------------------+------+-----+---------+----------------+
3.6 模型定义与数据库迁移流程
下图展示了从模型定义到数据库表生成的完整流程,以及标签在各个环节的作用:
3.7 完整模型示例:用户订单系统
综合以上知识点,下面是一个包含主键、自增、默认值、注释、索引和软删除的完整模型示例:
package model
import (
"time"
"gorm.io/gorm"
)
// User 用户表
type User struct {
ID uint `gorm:"primaryKey;autoIncrement;comment:用户ID"`
Username string `gorm:"type:varchar(50);uniqueIndex;not null;comment:用户名"`
Password string `gorm:"type:varchar(255);not null;comment:密码"`
Email string `gorm:"type:varchar(255);uniqueIndex;default:'';comment:邮箱"`
Phone string `gorm:"type:varchar(20);default:'';comment:手机号"`
Status int `gorm:"default:1;comment:状态 1-正常 0-禁用"`
CreatedAt time.Time `gorm:"comment:创建时间"`
UpdatedAt time.Time `gorm:"comment:更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间"`
}
// Order 订单表
type Order struct {
ID uint `gorm:"primaryKey;autoIncrement;comment:订单ID"`
OrderNo string `gorm:"type:varchar(32);uniqueIndex;not null;comment:订单号"`
UserID uint `gorm:"not null;index;comment:用户ID"`
TotalAmount float64 `gorm:"type:decimal(12,2);default:0.00;comment:订单总金额"`
Status int `gorm:"default:0;comment:订单状态 0-待支付 1-已支付 2-已发货 3-已完成"`
PayTime *time.Time `gorm:"comment:支付时间"`
CreatedAt time.Time `gorm:"comment:创建时间"`
UpdatedAt time.Time `gorm:"comment:更新时间"`
}
// OrderItem 订单明细表
type OrderItem struct {
ID uint `gorm:"primaryKey;autoIncrement;comment:明细ID"`
OrderID uint `gorm:"not null;index;comment:订单ID"`
ProductID uint `gorm:"not null;comment:产品ID"`
ProductName string `gorm:"type:varchar(200);not null;comment:产品名称"`
Price float64 `gorm:"type:decimal(10,2);not null;comment:单价"`
Quantity int `gorm:"default:1;comment:数量"`
SubTotal float64 `gorm:"type:decimal(12,2);default:0.00;comment:小计金额"`
}
迁移执行:
// 在 main.go 或初始化代码中执行迁移
db.AutoMigrate(&model.User{}, &model.Order{}, &model.OrderItem{})
GORM 会自动创建三张表,并根据标签生成对应的列类型、约束、索引和注释。
3.8 常见陷阱与避坑指南
| 陷阱 | 现象 | 解决方案 |
|---|---|---|
| 零值不触发默认值 | int 字段为 0 时,数据库默认值不生效 | 使用 *int 指针类型或 sql.NullInt64 |
| 标签语法错误 | 迁移报错或列属性异常 | 使用分号分隔,不要加多余空格 |
| 字段名与关键字冲突 | SQL 执行失败 | 使用 column 标签指定别名 |
| 时间字段时区问题 | 存储时间与预期不符 | 统一使用 time.Time 并配置数据库时区 |
| 软删除与唯一索引冲突 | 删除后无法创建相同唯一值 | 使用复合唯一索引包含 deleted_at 字段 |
重要提醒:模型定义是数据库契约的第一道防线。在团队协作中,模型文件应作为代码评审的重点,确保每个字段的标签完整、准确,避免后期迁移和校验时出现意外差异。