Go database/sql Null[T] 怎么扫描可空字段
来源:17golang原创
时间:2026-09-27 17:15:55 156浏览 收藏
我第一次把用户资料从数据库读进 Go 结构体时,最容易忽略的不是 SQL,而是“没有值”和“值为零”并不是一回事。昵称为 SQL NULL,年龄为 0,空字符串,这三种状态在业务上可能完全不同。直接把可空列扫描到 string 或 int64,通常会在边界数据上暴露问题。
结论先说:Go 1.22 起可以用 sql.Null[T] 作为 Scan 目标。它用 V 保存值,用 Valid 表示当前列是否不是 SQL NULL;扫描时传指针,读取时先判断 Valid,写回时让它的 Value 保留空值语义。
官方文档:https://pkg.go.dev/database/sql
sql.Null[T]在 Go 1.22 加入,结构核心是V和Valid。Scan的目标要传&nullable,不能传值副本。Valid=false表示 SQL NULL,不代表V一定是业务默认值。
先把 NULL 当成需要保护的业务资产
可空字段的资产是“存在性信息”。例如会员表的 nickname 为 NULL 可能代表用户从未设置昵称,空字符串则可能代表用户主动清空;把二者都写成 "",后面的展示、补全和审计就失去了判断依据。
我会先做一个小映射,再写扫描代码:
| 数据库结果 | Go 目标 | 读取规则 |
|---|---|---|
| NULL | sql.Null[string] | Valid=false |
| 非 NULL 文本 | sql.Null[string] | Valid=true 后读取 V |
| 可空整数 | sql.Null[int64] | 只让数值转换落在驱动支持范围内 |
| 可空时间 | sql.Null[time.Time] | 确认驱动返回的时间类型 |
这里的风险不是“程序马上崩溃”,而是默认值穿过权限、计费或展示逻辑后变成一条看似合理的错误数据。只有把 NULL 视为输入边界,后面的控制才有意义。
用 *sql.Null[T] 扫描可空字段
最小写法是在 Go 1.22 或更高版本中声明泛型 Null,并把它的地址交给 Scan。下面的代码以单行查询为例,重点是指针、错误处理和读取顺序。
package main
import (
"database/sql"
"fmt"
)
type User struct {
ID int64
Nickname sql.Null[string]
Age sql.Null[int64]
}
func loadUser(db *sql.DB, id int64) (User, error) {
var user User
// Scan 需要可写的目标;传地址才能让 Null[T].Scan 更新 V 和 Valid。
err := db.QueryRow("SELECT id, nickname, age FROM users WHERE id = ?", id).
Scan(&user.ID, &user.Nickname, &user.Age)
if err != nil {
// ErrNoRows、驱动转换错误都要交给上层决定如何处理。
return User{}, err
}
return user, nil
}
func displayNickname(user User) string {
// Valid=false 时代表数据库是 NULL,不把 V 当成用户输入。
if !user.Nickname.Valid {
return "(未设置)"
}
return user.Nickname.V
}
func _() {
_ = fmt.Sprint
}
Null[T] 实现了 sql.Scanner,所以 Rows.Scan 会把数据库驱动传来的值交给它处理。列为 NULL 时,Valid 变成 false;列有值时,V 才是可交给业务层的结果。这里不要写成 Scan(user.Nickname),那只是一个不可写的值副本。
用 Valid 切断零值误判和输入污染
可空类型的安全控制点只有一个:业务分支必须先看 Valid。不能因为 V 的零值“看起来能用”,就跳过存在性判断。
func ageLabel(age sql.Null[int64]) string {
// NULL 不等于 0;先分流才能避免把未知年龄显示成零岁。
if !age.Valid {
return "年龄未填写"
}
if age.V
还要留意泛型参数的边界。官方文档要求 T 选择驱动值支持的类型;不能把它当成任意结构体的通用反序列化器。如果驱动返回的值无法无损转换,Scan 应该返回错误,而不是猜一个默认值。
![Go database/sql Null[T] 从数据库 NULL 或实际值进入 V 与 Valid 的分流结构说明图](/uploads/20260927/1790500554-f85b9bfc1c-42f217162b-nullt-scan-boundary.webp)
批量读取与写回时保留同一条边界
批量查询不能只检查每次 Scan。每行处理完后还要检查 rows.Err,否则网络或驱动在迭代末尾报告的错误可能被忽略。对大结果集,我通常把可空字段保持在数据访问层,离开这一层后再转换成明确的展示模型。
func listUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT id, nickname, age FROM users ORDER BY id")
if err != nil {
return nil, err
}
defer rows.Close() // 及时归还连接,避免异常分支占住连接池。
users := make([]User, 0)
for rows.Next() {
var user User
// 每次循环都把地址传给 Scan,并立即处理本行转换错误。
if err := rows.Scan(&user.ID, &user.Nickname, &user.Age); err != nil {
return nil, err
}
users = append(users, user)
}
if err := rows.Err(); err != nil {
// Next 返回 false 既可能是结束,也可能是迭代期间出错。
return nil, err
}
return users, nil
}
func keepNullableValue(value sql.Null[string]) any {
// Value 会把 Valid=false 还原为 SQL NULL,适合写回参数。
return value
}
写回时,Null[T].Value()实现了 driver.Valuer,可以把无效值保留为 SQL NULL。若业务需求是“空字符串代表清空”,就显式构造一个 sql.Null[string]{V: "", Valid: true},不要依靠结构体零值碰运气。
| 检查项 | 通过条件 | 失败处理 |
|---|---|---|
| 版本 | Go 1.22+ | 旧版本使用专用 Null 类型或自定义 Scanner |
| 目标 | *sql.Null[T] | 修正为指针,避免扫描到副本 |
| 状态 | 先判断 Valid | 不要用 V 的零值猜测 NULL |
| 批量结果 | 检查 Scan 与 rows.Err | 记录驱动或连接错误并回滚本行处理 |
![Go Null[T] 在批量 Rows.Scan 和 Value 写回之间保持 NULL 语义的边界结构图](/uploads/20260927/1790500553-f85b9bfc1c-6bb5c6c380-nullt-batch-writeback.webp)
常见问题
sql.Null[T] 支持任意 Go 结构体吗?
不支持。T 应选择数据库驱动能提供和转换的值类型;复杂结构体需要单独实现 Scanner 与 Valuer。
Valid=false 时 V 还能读取吗?
可以读取语言层面的字段,但它不代表数据库真实存储的值。业务判断应把 Valid=false 当成 NULL 分支。
Go 1.21 项目能直接使用 Null[T] 吗?
不能直接使用标准库中的该类型。可以继续使用 sql.NullString、sql.NullInt64 等类型,或把自定义可空类型实现为 Scanner/Valuer,等升级到 Go 1.22 后再迁移。
对我来说,sql.Null[T]真正省下的是重复定义可空包装类型的成本,而不是替业务做决定。把 Valid 当作不可跳过的边界检查,再用错误处理和 rows.Err 收口,扫描可空字段就不会因为一个零值而悄悄改变含义。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
167 收藏
-
197 收藏
-
342 收藏
-
138 收藏
-
440 收藏
-
474 收藏
-
304 收藏
-
359 收藏
-
160 收藏
-
232 收藏
-
465 收藏
-
Golang · Go教程 | 12小时前 | 错误处理 · 数据校验 · Go教程 · Go compress/gzip ErrChecksum gzip.Reader io.ErrUnexpectedEOF347 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习