sql.NullString不能直接当string用,必须先检查Valid字段是否为true,Valid为false时表示数据库值为NULL,此时String字段不可信;扫描时需传地址&user.Field,写入NULL需设Valid=false,存空字符串则Valid=true且String=""。

sql.NullString 不能直接当 string 用,必须检查 Valid
Go 原生 database/sql 扫描 NULL 到普通 string 会 panic:sql: Scan error on column index 0: unsupported Scan, storing driver.Value type <nil> into type *string</nil>。GORM 虽然封装了一层,但底层仍走这套逻辑——只要字段允许 NULL,且你结构体里声明的是 string,就会报错或静默丢数据。
正确做法是改用 sql.NullString,但它不是字符串,而是带 Valid 字段的结构体:
type NullString struct {
String string
Valid bool
}
关键点:
-
Valid == false表示数据库值为 NULL,此时String字段内容不可信(可能是零值、旧值或未初始化) - JSON 序列化时默认输出
{"String":"xxx","Valid":true},业务接口通常要转成"xxx"或null,不能直接透出 - 赋值给结构体字段时,必须传指针:
&user.Nickname,否则Valid永远不会被设为true - 写回数据库时:想存 NULL 就设
Nickname.Valid = false;想存空字符串就设Nickname.Valid = true且Nickname.String = ""
GORM 中用指针类型替代 sql.NullX 是更简洁的选择
比起每个字段都写 sql.NullString、sql.NullInt64,GORM 支持直接用 Go 原生指针类型(如 *string、*int64)来表达可空语义,代码更干净,也避免混用类型出错。
例如:
type User struct {
ID uint `gorm:"primaryKey"`
Name *string `gorm:"column:name"`
Age *int64 `gorm:"column:age"`
}
优势与注意事项:
- 扫描时自动处理 NULL →
nil,非 NULL → 分配新内存并赋值,无需手动判断Valid - JSON 输出天然兼容:
*string为nil时序列化为null,有值时为字符串,不用额外转换 - 写入时:传
nil即存 NULL;传&"foo"即存字符串;比sql.NullString少两步操作 - 注意:指针类型字段在 GORM 更新时,如果值为
nil,默认不参与 UPDATE(GORM 认为“未提供”),需显式用Select()或Updates()控制字段集
GORM AutoMigrate 不会帮你补全 NULL 约束,字段定义必须显式声明
AutoMigrate 只根据结构体字段类型和 tag 推导数据库列是否允许 NULL,但它不会主动把已有 NOT NULL 字段改成允许 NULL,也不会把允许 NULL 的字段加 DEFAULT —— 它只做“最小必要变更”。
常见翻车点:
- 结构体字段是
*string,但数据库列是NOT NULL,插入时会报错Column 'name' cannot be null - 结构体字段是
string(非指针),但数据库列允许 NULL,扫描时直接 panic,AutoMigrate却不提醒你改结构体 - MySQL 8+ 严格模式下,若字段类型不匹配(比如 Go
int对应TINYINT,但库中已是INT),AutoMigrate会跳过该字段且无提示
建议做法:
- 建模时就明确字段是否可空,并统一用
*T或sql.NullT,别混用 - 首次建表后,用
db.Migrator().CurrentDatabase()验证连接,再执行AutoMigrate - 生产环境禁用
AutoMigrate,改用人工审核的 SQL 迁移脚本
更新时零值(0、""、false)被忽略,是因为 GORM 默认跳过零值字段
GORM 在调用 Save 或 Update 时,默认过滤掉所有零值字段(0、""、false、nil 等),这会导致你想把 Age 设为 0 却失败,数据库里还是旧值。
根本原因:GORM 把零值当作“未设置”,而非“明确设为零”。解决方案分场景:
- 用
map[string]interface{}显式传参:db.Model(&u).Updates(map[string]interface{}{"age": 0}) - 用
Select()强制指定字段:db.Select("age").Model(&u).Update("age", 0) - 字段改用
sql.NullInt64或*int64,这样0和nil可区分(nil→ NULL,&0→ 0) - 全局关闭零值忽略(不推荐):
gorm.Config{SkipDefaultTransaction: true}不起作用;真正有效的是自定义NamingStrategy或重写BeforeUpdate钩子
最稳妥的路径,是把「业务上需要区分『未填写』和『填了 0』」的字段,全部改用指针或 sql.NullX 类型——这是 GORM 处理 NULL 最不容易出错的底座。


















