Skip to content

第06章:模型标签全解(上)—— 字段定义 ​

本章目标 ​

学完本章后,你将能够:

  1. 使用 column、type、size 等标签精确控制列的定义
  2. 使用 primaryKey、autoIncrement 管理主键
  3. 使用 default、not null、unique 设置字段约束
  4. 使用 index、uniqueIndex 创建数据库索引
  5. 使用 <-、->、- 控制字段的读写权限
  6. 理解 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:自定义列名 ​

go
type User struct {
	ID       uint   `gorm:"column:user_id"`      // 列名变为 user_id
	FullName string `gorm:"column:full_name"`    // 列名变为 full_name
}

生成的 SQL:

sql
CREATE TABLE users (
    user_id   BIGINT UNSIGNED AUTO_INCREMENT,
    full_name VARCHAR(255)
)

📌 如果没有 column 标签,FullName 默认列名是 full_name。大多数情况下默认命名已经够用了,只在需要特殊列名时使用 column。

type:指定数据库列类型 ​

GORM 会依据 Go 类型自动推断数据库类型,但有时你需要精确控制:

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:

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 主要用于字符串和整数,控制列的宽度:

go
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 是小数位数:

go
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.9999

6.3 约束与索引标签 ​

primaryKey:主键 ​

go
// 默认已有主键行为(ID 字段),但可以手动指定
type User struct {
	UUID string `gorm:"primaryKey;type:varchar(36)"`  // 用 UUID 做主键
}
go
// 复合主键(两个字段共同作为主键)
type OrderItem struct {
	OrderID   uint `gorm:"primaryKey"`
	ProductID uint `gorm:"primaryKey"`
	Quantity  int
}
// GENERATED SQL: PRIMARY KEY (order_id, product_id)

autoIncrement:自增 ​

go
type User struct {
	ID uint `gorm:"primaryKey;autoIncrement"`
}

📌 uint 或 int 类型 + 名为 ID 的字段,GORM 自动设置为自增主键,不需要显式加 autoIncrement。

default:默认值 ​

go
type User struct {
	Name   string `gorm:"default:'匿名用户'"`  // 字符串默认值用单引号包裹
	Age    int    `gorm:"default:18"`
	Status string `gorm:"default:'active'"`
}

生成的 SQL:

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:非空约束 ​

go
type User struct {
	Name  string `gorm:"not null"`
	Email string `gorm:"not null"`
	Age   int    `gorm:"not null"`
}

生成的 SQL:

sql
CREATE TABLE users (
    name  VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL,
    age   BIGINT NOT NULL
)

unique:唯一约束 ​

go
type User struct {
	Email    string `gorm:"unique"`      // 唯一约束
	Username string `gorm:"unique"`      // 唯一约束
}

生成的 SQL:

sql
CREATE TABLE users (
    email    VARCHAR(255),
    username VARCHAR(255),
    UNIQUE INDEX `idx_users_email` (`email`),
    UNIQUE INDEX `idx_users_username` (`username`)
)

uniqueIndex:唯一索引 ​

go
type User struct {
	Email string `gorm:"uniqueIndex"`       // 唯一索引
	Phone string `gorm:"uniqueIndex:idx_phone"`  // 指定索引名
}

生成的 SQL:

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:普通索引 ​

go
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:

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:检查约束 ​

go
type User struct {
	Age  int `gorm:"check:age > 0 AND age < 150"`
}

生成的 SQL(MySQL 8.0.16+ 才真正支持):

sql
CREATE TABLE users (
    age BIGINT,
    CONSTRAINT `chk_users_age` CHECK (age > 0 AND age < 150)
)

comment:列注释 ​

go
type User struct {
	Name string `gorm:"comment:用户真实姓名"`
	Age  int    `gorm:"comment:年龄,必须大于0"`
}

生成的 SQL:

sql
CREATE TABLE users (
    name VARCHAR(255) COMMENT '用户真实姓名',
    age  BIGINT COMMENT '年龄,必须大于0'
)

📌 强烈建议:生产环境给每个字段加上 comment,方便 DBA 和后续维护。


6.4 读写权限控制标签 ​

这对某些特殊场景非常有用:

权限控制标签一览 ​

标签含义何时使用
<-可读可写(默认)大多数字段
<-:false禁止写入自动计算的字段
<-:create只允许创建时写入创建后不可修改的字段
<-:update只允许更新时写入只能在更新时设置的字段
->只读同上(-> 等同于 <-:false)
->:false禁止读取密码等敏感字段(不查出来)
-完全忽略GORM 不管理这个字段

示例 ​

go
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 标签组合实战 ​

把上面学的标签组合起来,定义一个生产级的用户模型:

go
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(在你脑中过一遍,看看能不能对上):

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 标签的零值陷阱 ​

go
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 值忘记加引号 ​

go
// ❌ 错误写法
Name string `gorm:"default:匿名用户"`
// 生成的 SQL:DEFAULT 匿名用户(缺少引号,SQL 语法错误)

// ✅ 正确写法
Name string `gorm:"default:'匿名用户'"`

错误3:unique 和 uniqueIndex 混淆 ​

在 MySQL 中两者实际效果相同,但 uniqueIndex 更灵活(可以指定索引名)。统一用 uniqueIndex 是最佳实践。

错误4:给不需要索引的字段加索引 ​

go
// ❌ 过度索引
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 标签存在零值陷阱,下一章会给出解决方案

练习题 ​

  1. 定义一个 Product 模型,要求:
    • Name 类型为 varchar(200),不能为空,带注释
    • Price 类型为 decimal(10,2),默认值为 0.00
    • Stock 类型为 int unsigned,默认值为 0
    • Status 有普通索引
    • Sku 有唯一索引
  2. default 标签的零值陷阱是什么?举一个具体的例子。
  3. unique 和 uniqueIndex 有什么区别?应该用哪个?
  4. 如何让 GORM 的密码字段不被查询出来?
  5. 以下标签各有什么作用:<-:create、<-:false、->:false、-、-:migration?
  6. (思考题)如果需要在多个字段上创建复合索引(如 (status, created_at)),标签应该怎么写?

把零散知识,整理成可以反复走的路。