第06章:模型标签全解(上)—— 字段定义
本章目标
学完本章后,你将能够:
- 使用
column、type、size等标签精确控制列的定义 - 使用
primaryKey、autoIncrement管理主键 - 使用
default、not null、unique设置字段约束 - 使用
index、uniqueIndex创建数据库索引 - 使用
<-、->、-控制字段的读写权限 - 理解
default标签的零值陷阱
前置知识
- 需要先学习:第05章(模型定义基础)
- 需要了解:MySQL 的 DDL 基本操作(CREATE TABLE、数据类型、约束、索引)
6.1 标签全景图
GORM 模型标签分为三大类:
| 分类 | 标签示例 | 作用 |
|---|---|---|
| 字段定义 | column、type、size、precision、scale | 控制列名和数据类型 |
| 约束与索引 | primaryKey、autoIncrement、not null、default、unique、index、check、comment | 设置列约束和索引 |
| 读写控制 | <-、->、- | 控制字段的读写权限 |
本章逐一讲解,每个标签都配有代码示例和执行后的 SQL。
6.2 字段定义标签
column:自定义列名
type User struct {
ID uint `gorm:"column:user_id"` // 列名变为 user_id
FullName string `gorm:"column:full_name"` // 列名变为 full_name
}生成的 SQL:
CREATE TABLE users (
user_id BIGINT UNSIGNED AUTO_INCREMENT,
full_name VARCHAR(255)
)📌 如果没有
column标签,FullName默认列名是full_name。大多数情况下默认命名已经够用了,只在需要特殊列名时使用column。
type:指定数据库列类型
GORM 会依据 Go 类型自动推断数据库类型,但有时你需要精确控制:
type Product struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(200)"` // VARCHAR(200)
Description string `gorm:"type:text"` // TEXT
Price float64 `gorm:"type:decimal(10,2)"` // DECIMAL(10,2)
Stock int `gorm:"type:int unsigned"` // INT UNSIGNED
Weight float64 `gorm:"type:double(8,2)"` // DOUBLE(8,2)
Avatar []byte `gorm:"type:mediumblob"` // MEDIUMBLOB
IsOnSale bool `gorm:"type:tinyint(1)"` // TINYINT(1)
}生成的 SQL:
CREATE TABLE products (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200),
description TEXT,
price DECIMAL(10,2),
stock INT UNSIGNED,
weight DOUBLE(8,2),
avatar MEDIUMBLOB,
is_on_sale TINYINT(1)
)size:设置列大小
size 主要用于字符串和整数,控制列的宽度:
type User struct {
Username string `gorm:"size:50"` // → VARCHAR(50)
Code string `gorm:"size:10"` // → VARCHAR(10)
Age int `gorm:"size:3"` // → TINYINT(因为能用3位表示)
Score int `gorm:"size:5"` // → SMALLINT
// int 用 size 只影响底层类型选择,不影响实际存储范围
}📌
size和type的区别:type:varchar(100)明确指定了类型和大小。size:100只指定大小,类型由 GORM 从 Go 类型推断。
precision / scale:小数精度
用于 DECIMAL 类型,precision 是总位数,scale 是小数位数:
type Product struct {
Price float64 `gorm:"precision:10;scale:2"` // → DECIMAL(10,2)
TaxRate float64 `gorm:"precision:5;scale:4"` // → DECIMAL(5,4)
}
// Price: 10位总精度,其中2位小数 → 最大 99999999.99
// TaxRate: 5位总精度,其中4位小数 → 最大 9.99996.3 约束与索引标签
primaryKey:主键
// 默认已有主键行为(ID 字段),但可以手动指定
type User struct {
UUID string `gorm:"primaryKey;type:varchar(36)"` // 用 UUID 做主键
}// 复合主键(两个字段共同作为主键)
type OrderItem struct {
OrderID uint `gorm:"primaryKey"`
ProductID uint `gorm:"primaryKey"`
Quantity int
}
// GENERATED SQL: PRIMARY KEY (order_id, product_id)autoIncrement:自增
type User struct {
ID uint `gorm:"primaryKey;autoIncrement"`
}📌
uint或int类型 + 名为ID的字段,GORM 自动设置为自增主键,不需要显式加autoIncrement。
default:默认值
type User struct {
Name string `gorm:"default:'匿名用户'"` // 字符串默认值用单引号包裹
Age int `gorm:"default:18"`
Status string `gorm:"default:'active'"`
}生成的 SQL:
CREATE TABLE users (
name VARCHAR(255) DEFAULT '匿名用户',
age BIGINT DEFAULT 18,
status VARCHAR(255) DEFAULT 'active'
)⚠️ 零值陷阱(极其重要):
default标签只在 GORM 认为字段是"零值"时才生效。对于string类型,""是零值,GORM 认为"没设置",会使用数据库默认值。但对于int类型,0是零值,GORM 会认为你"没有设置 Age",从而使用 default。如果你真想把 Age 设置为 0,GORM 会跳过它!这就是很多 GORM 新手遇到的诡异问题——"我明明设了 Age=0,为什么数据库里存的是默认值 18?"
解决办法见下一章(使用指针类型或 Map)。
not null:非空约束
type User struct {
Name string `gorm:"not null"`
Email string `gorm:"not null"`
Age int `gorm:"not null"`
}生成的 SQL:
CREATE TABLE users (
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
age BIGINT NOT NULL
)unique:唯一约束
type User struct {
Email string `gorm:"unique"` // 唯一约束
Username string `gorm:"unique"` // 唯一约束
}生成的 SQL:
CREATE TABLE users (
email VARCHAR(255),
username VARCHAR(255),
UNIQUE INDEX `idx_users_email` (`email`),
UNIQUE INDEX `idx_users_username` (`username`)
)uniqueIndex:唯一索引
type User struct {
Email string `gorm:"uniqueIndex"` // 唯一索引
Phone string `gorm:"uniqueIndex:idx_phone"` // 指定索引名
}生成的 SQL:
CREATE TABLE users (
email VARCHAR(255),
phone VARCHAR(255),
UNIQUE INDEX `idx_users_email` (`email`),
UNIQUE INDEX `idx_phone` (`phone`)
)📌
unique和uniqueIndex在 MySQL 中效果相同。区别是uniqueIndex可以指定索引名,推荐使用。
index:普通索引
type User struct {
Status string `gorm:"index"` // 普通索引,自动命名
Age int `gorm:"index:idx_users_age"` // 指定索引名
City string `gorm:"index:idx_city_status,priority:1"` // 复合索引 + 优先级
State string `gorm:"index:idx_city_status,priority:2"`
}生成的 SQL:
CREATE TABLE users (
status VARCHAR(255),
age BIGINT,
city VARCHAR(255),
state VARCHAR(255),
INDEX `idx_users_status` (`status`),
INDEX `idx_users_age` (`age`),
INDEX `idx_city_status` (`city`, `state`)
)📌
priority控制字段在复合索引中的位置。priority:1排第一,priority:2排第二。
check:检查约束
type User struct {
Age int `gorm:"check:age > 0 AND age < 150"`
}生成的 SQL(MySQL 8.0.16+ 才真正支持):
CREATE TABLE users (
age BIGINT,
CONSTRAINT `chk_users_age` CHECK (age > 0 AND age < 150)
)comment:列注释
type User struct {
Name string `gorm:"comment:用户真实姓名"`
Age int `gorm:"comment:年龄,必须大于0"`
}生成的 SQL:
CREATE TABLE users (
name VARCHAR(255) COMMENT '用户真实姓名',
age BIGINT COMMENT '年龄,必须大于0'
)📌 强烈建议:生产环境给每个字段加上
comment,方便 DBA 和后续维护。
6.4 读写权限控制标签
这对某些特殊场景非常有用:
权限控制标签一览
| 标签 | 含义 | 何时使用 |
|---|---|---|
<- | 可读可写(默认) | 大多数字段 |
<-:false | 禁止写入 | 自动计算的字段 |
<-:create | 只允许创建时写入 | 创建后不可修改的字段 |
<-:update | 只允许更新时写入 | 只能在更新时设置的字段 |
-> | 只读 | 同上(-> 等同于 <-:false) |
->:false | 禁止读取 | 密码等敏感字段(不查出来) |
- | 完全忽略 | GORM 不管理这个字段 |
示例
type User struct {
ID uint `gorm:"primaryKey"`
// 普通字段,可读可写(默认行为,不需要标签)
Name string
// 只写不读:查询时不会 SELECT 这个字段
Password string `gorm:"->:false"`
// 只读不写:只能从数据库读取,写入时忽略此字段
SignupAt time.Time `gorm:"<-:false"`
// 只允许创建时写入
CreatedBy string `gorm:"<-:create"`
// 只允许更新时写入
UpdatedBy string `gorm:"<-:update"`
// 完全忽略:GORM 当作这个字段不存在
TempData string `gorm:"-"`
// 忽略迁移(但正常读写)
ComputedField string `gorm:"-:migration"`
}6.5 标签组合实战
把上面学的标签组合起来,定义一个生产级的用户模型:
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;->:false;comment:密码(不可读)"`
RealName string `gorm:"type:varchar(50);not null;default:'';comment:真实姓名"`
Age int `gorm:"type:tinyint unsigned;default:0;check:age >= 0 AND age <= 150;comment:年龄"`
Email string `gorm:"type:varchar(255);uniqueIndex;not null;comment:邮箱"`
Phone string `gorm:"type:varchar(20);index;comment:手机号"`
Balance float64 `gorm:"type:decimal(12,2);default:0.00;comment:账户余额"`
Status string `gorm:"type:varchar(20);default:'active';index:idx_status_created;priority:1;comment:用户状态"`
CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间"`
}生成的 SQL(在你脑中过一遍,看看能不能对上):
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT COMMENT '用户ID',
username VARCHAR(50) NOT NULL COMMENT '用户名',
password VARCHAR(255) NOT NULL COMMENT '密码(不可读)',
real_name VARCHAR(50) NOT NULL DEFAULT '' COMMENT '真实姓名',
age TINYINT UNSIGNED DEFAULT 0 COMMENT '年龄'
CHECK (age >= 0 AND age <= 150),
email VARCHAR(255) NOT NULL COMMENT '邮箱',
phone VARCHAR(20) COMMENT '手机号',
balance DECIMAL(12,2) DEFAULT 0.00 COMMENT '账户余额',
status VARCHAR(20) DEFAULT 'active' COMMENT '用户状态',
created_at DATETIME(3) COMMENT '创建时间',
updated_at DATETIME(3) COMMENT '更新时间',
deleted_at DATETIME(3) COMMENT '删除时间',
PRIMARY KEY (`id`),
UNIQUE INDEX `idx_users_username` (`username`),
UNIQUE INDEX `idx_users_email` (`email`),
INDEX `idx_users_phone` (`phone`),
INDEX `idx_status_created` (`status`, `created_at`),
INDEX `idx_users_deleted_at` (`deleted_at`)
)常见错误
错误1:default 标签的零值陷阱
type User struct {
Age int `gorm:"default:18"`
}
// 我想把 Age 设为 0
user := User{Name: "张三", Age: 0}
db.Create(&user)
// ❌ 生成的 SQL:INSERT INTO users (name) VALUES ('张三')
// 数据库中 Age = 18(使用了 default!)
// 因为 Age=0 是 int 的零值,GORM 认为"你没设置"错误2:字符串 default 值忘记加引号
// ❌ 错误写法
Name string `gorm:"default:匿名用户"`
// 生成的 SQL:DEFAULT 匿名用户(缺少引号,SQL 语法错误)
// ✅ 正确写法
Name string `gorm:"default:'匿名用户'"`错误3:unique 和 uniqueIndex 混淆
在 MySQL 中两者实际效果相同,但 uniqueIndex 更灵活(可以指定索引名)。统一用 uniqueIndex 是最佳实践。
错误4:给不需要索引的字段加索引
// ❌ 过度索引
type User struct {
Avatar string `gorm:"index"` // 头像URL加索引?通常不需要
Bio string `gorm:"index"` // 个人简介加索引?也不需要
}
// ✅ 给高频搜索字段加索引
type User struct {
Email string `gorm:"uniqueIndex"` // 登录搜索 → 需要索引
Status string `gorm:"index"` // 按状态筛选 → 需要索引
}本章小结
column/type/size/precision/scale控制字段到列的精确映射primaryKey/autoIncrement/not null/default/unique控制约束index/uniqueIndex创建数据库索引,能大幅提升查询性能check/comment是 MySQL 8.0+ 推荐使用的约束和文档工具<-/->/-控制读写权限,->:false可以阻止密码等字段被读取default标签存在零值陷阱,下一章会给出解决方案
练习题
- 定义一个
Product模型,要求:- Name 类型为
varchar(200),不能为空,带注释 - Price 类型为
decimal(10,2),默认值为 0.00 - Stock 类型为
int unsigned,默认值为 0 - Status 有普通索引
- Sku 有唯一索引
- Name 类型为
default标签的零值陷阱是什么?举一个具体的例子。unique和uniqueIndex有什么区别?应该用哪个?- 如何让 GORM 的密码字段不被查询出来?
- 以下标签各有什么作用:
<-:create、<-:false、->:false、-、-:migration? - (思考题)如果需要在多个字段上创建复合索引(如
(status, created_at)),标签应该怎么写?