database/sql Null 类型扫描到业务结构体的转换
如果数据库列允许 NULL,把查询结果直接扫描到普通的 string、int64 或 time.Time 字段,常见结果是 Scan 报错,或者业务代码把“没有值”和“值为零”混在一起。稳定的做法是:先在数据库访问边界使用 sql.NullString、sql.NullInt64 等可空类型,读取它们的 Valid 字段,再转换成业务结构体需要的指针、可选值或明确默认值。
- 数据库驱动把 SQL
NULL传给Scanner时,对应的是nil;普通值类型不能表达这个状态。 sql.Null*用“值字段 +Valid”保留空值语义;支持 Go 1.22 及以上时也可以使用sql.Null[T]。- 推荐把 Null 类型限制在持久化 DTO 中,再显式映射到业务结构体,避免数据库适配细节渗透到业务规则。
先还原 NULL 扫描失败的现场
database/sql.Scanner 接收的源值包含整数、浮点数、布尔值、字节切片、字符串、时间以及表示 SQL NULL 的 nil。当目标是普通的 string 或 int64 时,它们没有“缺失”这个额外状态,因此 Scan 无法在不丢失信息的前提下完成转换。
type User struct {
ID int64
Nickname string
}
var user User
err := db.QueryRowContext(ctx, `SELECT id, nickname FROM users WHERE id = ?`, id).Scan(&user.ID, &user.Nickname)
if err != nil {
// nickname 为 NULL 时,普通 string 无法承载缺失状态,错误会在 Scan 处返回。
return fmt.Errorf("读取用户失败: %w", err)
}
这里的问题不是 SQL 语句一定错误,而是查询列的可空性与 Go 目标字段的表达能力不匹配。更重要的是,直接把 NULL 改成空字符串也会丢失信息:空字符串可能是用户主动保存的值,而 NULL 可能代表用户从未填写。

在扫描边界使用 sql.Null 类型
database/sql 提供了一组实现 Scanner 和 driver.Valuer 的类型。以字符串为例,sql.NullString.String 保存实际值,sql.NullString.Valid 表示该值是否来自非 NULL 数据。数字类型的规则相同,例如 sql.NullInt64.Int64 和 sql.NullInt64.Valid。
type userRow struct {
ID int64
Nickname sql.NullString
Age sql.NullInt64
}
var row userRow
err := db.QueryRowContext(ctx, `
SELECT id, nickname, age
FROM users
WHERE id = ?
`, id).Scan(&row.ID, &row.Nickname, &row.Age)
if err != nil {
// 查询失败和 NULL 本身是两件事;先处理查询错误,再读取 Valid。
return fmt.Errorf("扫描用户行失败: %w", err)
}
if row.Nickname.Valid {
log.Printf("昵称=%q", row.Nickname.String)
}
if !row.Age.Valid {
// Age 为 NULL,不要把它误报成 0 岁。
log.Println("用户没有填写年龄")
}
这个边界有两个好处:扫描阶段不会把 SQL 空值强行塞进普通字段,业务代码也能明确区分“字段没有值”和“字段值恰好为零”。如果使用 Rows 批量读取,仍然要在每一轮先检查 Scan,循环结束后再检查 rows.Err()。
把数据库 DTO 映射成业务结构体
持久化结构体可以接受 sql.Null*,但不建议让所有业务函数都知道 NullString.String 和 Valid。更清楚的分层方式是:查询得到一个数据库 DTO,随后在一个转换函数中把可空字段映射成业务层的指针或自定义可选类型。
type User struct {
ID int64
Nickname *string
Age *int64
}
func (r userRow) toUser() User {
user := User{ID: r.ID}
if r.Nickname.Valid {
// 只有 Valid 为 true 才把数据库值暴露给业务层。
nickname := r.Nickname.String
user.Nickname = &nickname
}
if r.Age.Valid {
// 指针让业务层继续区分 NULL 与数值 0。
age := r.Age.Int64
user.Age = &age
}
return user
}
映射之后,业务代码可以通过 user.Nickname == nil 判断缺失,也可以在接口层统一把 nil 序列化为 JSON null。如果业务定义明确规定缺失字段必须显示为默认文案,也应在业务层做这个决定,而不是在 Scan 时静默替换。

按工具链选择泛型 Null[T]
如果项目使用 Go 1.22 或更高版本,可以用 sql.Null[T] 表达可空值。它把值放在 V 字段中,把是否有效放在 Valid 字段中,减少为不同基础类型分别记忆字段名的成本。
type userRowV2 struct {
ID int64
Nickname sql.Null[string]
Age sql.Null[int64]
}
var row userRowV2
if err := db.QueryRowContext(ctx, `
SELECT id, nickname, age FROM users WHERE id = ?
`, id).Scan(&row.ID, &row.Nickname, &row.Age); err != nil {
// 泛型 Null 仍然需要在 Scan 阶段检查错误。
return fmt.Errorf("读取用户失败: %w", err)
}
if row.Nickname.Valid {
// V 是值字段;Valid 为 false 时不要读取 V 作为业务结论。
fmt.Println(row.Nickname.V)
}
sql.Null[T] 的 T 不是任意业务对象,而应当是驱动值可以支持的类型。旧工具链、需要兼容旧版本的公共库,或者团队已经统一使用 NullString 等类型时,继续使用具体类型也完全合理;选择的关键是保持项目工具链和数据边界的一致。
批量查询和写回时保持同一套语义
批量查询时,Null 类型的使用方式不会因为结果集变多而改变。每行先扫描到 DTO,再转换到业务对象;循环结束后检查 rows.Err(),这样可以把扫描错误和迭代错误区分开。
rows, err := db.QueryContext(ctx, `SELECT id, nickname FROM users WHERE state = ?`, "active")
if err != nil {
// QueryContext 尚未返回结果集时,直接处理建立阶段错误。
return err
}
defer rows.Close()
for rows.Next() {
var row userRow
if err := rows.Scan(&row.ID, &row.Nickname); err != nil {
// 当前行扫描失败时立即停止,避免把不完整数据交给业务层。
return fmt.Errorf("扫描用户列表失败: %w", err)
}
consumeUser(row.toUser())
}
if err := rows.Err(); err != nil {
// Next 返回 false 不一定是正常结束,迭代阶段错误在这里收口。
return fmt.Errorf("读取用户列表失败: %w", err)
}
return nil
写回数据库时,sql.NullString 等类型实现了 driver.Valuer。当 Valid 为 false 时,传给驱动的值会保持为空值语义;当 Valid 为 true 时,才写入内部值。
row := userRow{
ID: 1,
Nickname: sql.NullString{String: "阿岚", Valid: true},
}
_, err := db.ExecContext(ctx, `UPDATE users SET nickname = ? WHERE id = ?`, row.Nickname, row.ID)
if err != nil {
// Value 转换或执行阶段失败都要向上层返回,不能假设写回成功。
return fmt.Errorf("更新用户昵称失败: %w", err)
}
常见问题
为什么不直接把 NULL 当成空字符串?
因为空字符串是一个真实字符串值,而 NULL 表示没有值。两者在搜索、更新、接口展示和统计时可能有不同含义,应在业务规则明确之后再决定是否合并。
Valid 为 false 时,String 或 V 一定是空的吗?
不要依赖无效状态下的值字段。业务判断应当先看 Valid,只有为 true 时才读取 String、Int64 或 V。
业务结构体也可以直接使用 sql.NullString 吗?
可以,但会让业务层依赖数据库访问包。小型程序可以接受;如果项目需要清晰分层、多个存储实现或稳定的接口模型,使用 DTO 到业务结构体的显式映射更容易维护。
Rows.Scan 报错后还要检查 rows.Err 吗?
当前行的 Scan 错误应当立即处理;如果循环是因为 Next 返回 false 结束,仍然要检查 rows.Err(),确认不是迭代阶段错误。
总结
处理 database/sql 的 NULL,核心不是寻找一个默认值,而是先保留“有没有值”这个事实。使用 sql.Null* 或 sql.Null[T] 接住扫描结果,在 DTO 到业务结构体的转换处决定指针、默认值或其它领域表达,再用同一套规则处理批量读取和写回。这样既能避免 Scan 类型错误,也不会把 NULL 悄悄伪装成零值。
建筑项目竣工验收资料的归档范围
- 上一篇
- 建筑项目竣工验收资料的归档范围
- 下一篇
- 青瓷山谷与清晨薄雾手机壁纸提示词
-
- Golang · Go问答 | 32分钟前 |
- zip Reader 在 HTTP Range 数据上的读取方式
- 246浏览 收藏
-
- Golang · Go问答 | 42分钟前 |
- zip 文件名编码异常时的读取策略
- 476浏览 收藏
-
- Golang · Go问答 | 53分钟前 |
- zip 解包中的相对路径校验与目录穿越防护
- 479浏览 收藏
-
- Golang · Go问答 | 1小时前 | Go问答 · io.EOF ErrChecksum gzip.Reader gzip.Reset Go压缩读取 旧缓冲数据
- gzip Reader 复用后旧缓冲数据残留的处理
- 247浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- gzip Multistream 读取拼接压缩流的边界
- 140浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- database/sql 查询上下文取消后的 rows 状态
- 251浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- database/sql Conn.Raw 执行驱动级操作的边界
- 347浏览 收藏
-
- Golang · Go问答 | 1小时前 | 错误处理 · Go问答 · 权限错误 filepath.WalkDir Go目录遍历 filepath.SkipDir
- filepath.WalkDir 遇到权限目录的错误处理
- 430浏览 收藏
-
- Golang · Go问答 | 1小时前 | go · 文件系统 · Go 符号链接 Go 文件打开 filepath EvalSymlinks os Lstat 跨平台文件访问
- 文件符号链接在不同系统上的打开差异
- 464浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- os.Root 路径校验仍失败时的相对路径规则
- 494浏览 收藏
-
- Golang · Go问答 | 2小时前 | Go问答 · SetReadDeadline UDP ReadFrom UDP deadline Go UDP 超时
- UDP ReadFrom 不返回时的 deadline 设置方式
- 108浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 408次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 484次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 494次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 440次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 268次使用
-
- 用Nginx反向代理部署go写的网站。
- 2023-01-17 502浏览
-
- GoLand调式动态执行代码
- 2023-01-13 502浏览
-
- Go crypto/rand.Text 的长度为什么不是固定字符数
- 2026-10-04 501浏览
-
- Go strings.ToValidUTF8 清洗日志内容的边界
- 2026-10-03 501浏览
-
- Go tls.GetCertificate 为什么收不到空 ServerName 请求
- 2026-09-27 501浏览

