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. 使用指针类型 *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 模型定义与数据库迁移流程

下图展示了从模型定义到数据库表生成的完整流程,以及标签在各个环节的作用:

模型定义 → 数据库迁移流程 Go 结构体定义 type User struct { ... } gorm 标签解析 primaryKey / default / comment AutoMigrate 执行 db.AutoMigrate(&User{}) DDL 语句生成 CREATE TABLE ... 数据库表创建 users 表 + 约束 + 注释 契约校验 模型 vs 表结构对比 标签关键作用 • primaryKey → 主键约束 • autoIncrement → 自增属性 • default → 默认值 • comment → 字段注释 迁移注意事项 • 零值陷阱需指针或包装类型 • 复合主键不支持自增

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 字段
重要提醒:模型定义是数据库契约的第一道防线。在团队协作中,模型文件应作为代码评审的重点,确保每个字段的标签完整、准确,避免后期迁移和校验时出现意外差异。