当前位置:首页 > 文章列表 > Golang > Go问答 > database/sql Null 类型扫描到业务结构体的转换

database/sql Null 类型扫描到业务结构体的转换

来源:17golang原创 2026-10-10 19:28:16 0浏览 收藏

如果数据库列允许 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、driver nil、Scanner 与 sql.Null 类型和普通字段之间的静态结构关系图
图1:SQL NULL 扫描边界结构图,展示空值如何在 database/sql 与目标字段之间分流;这是静态说明图,不是运行截图。

在扫描边界使用 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 时静默替换。

查询列、UserRow 数据库 DTO、Valid 判断、User 业务结构体和 Value 写回之间的关系图
图2:数据库 DTO 到业务结构体的映射关系图,展示 Valid 判断与业务可选字段之间的职责边界;这是静态说明图,不是运行截图。

按工具链选择泛型 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 悄悄伪装成零值。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
建筑项目竣工验收资料的归档范围建筑项目竣工验收资料的归档范围
上一篇
建筑项目竣工验收资料的归档范围
青瓷山谷与清晨薄雾手机壁纸提示词
下一篇
青瓷山谷与清晨薄雾手机壁纸提示词
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    408次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    484次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    494次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    440次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    268次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码