当前位置:首页 > 文章列表 > Golang > Go问答 > Go sql.NullString 为什么 JSON 输出不是 null

Go sql.NullString 为什么 JSON 输出不是 null

来源:17golang原创 2026-10-26 19:40:29 0浏览 收藏

在接口联调时,数据库里的昵称明明是 NULL,Go 返回的 JSON 却变成了 {"String":"","Valid":false},这不是数据库驱动把空值改坏了,而是两个层次的“空”没有被混在一起。sql.NullString 负责承载 SQL NULL,encoding/json 负责编码 Go 结构体;默认情况下,后者不会把一个结构体自动变成 JSON null。

官方文档:https://pkg.go.dev/database/sql#NullString

要点速览
  • Scan 遇到数据库 NULL 时,会把 sql.NullString.Valid 设为 false,而不是让 sql.NullString 变成 nil。
  • NullString 的 String 和 Valid 是导出字段,encoding/json 会按结构体字段输出,所以结果是对象。
  • 要输出真正的 JSON null,在接口 DTO 层使用 *string,或给包装类型实现 MarshalJSON。

先看 NULL 是怎样进入 NullString 的

sql.NullString 不是“可为空的字符串指针”,而是一个带状态位的值类型。官方定义包含 String string 和 Valid bool:当数据库列有字符串时,Valid 为 true;当列值是 SQL NULL 时,Valid 为 false,此时不能只看 String 判断业务含义。

var nickname sql.NullString
err := row.Scan(&nickname)
if err != nil {
    // Scan 失败时保留原始错误,避免把数据库错误误判为空值。
    return fmt.Errorf("scan nickname: %w", err)
}

if nickname.Valid {
    // Valid 为 true 时,String 才代表数据库返回的字符串。
    fmt.Println("昵称:", nickname.String)
} else {
    // Valid 为 false 表示 SQL NULL,不等同于空字符串。
    fmt.Println("昵称未设置")
}

所以,数据库读取层需要同时保留“值”和“是否存在”两个信息。把它直接放进 API 响应结构体,相当于把这两个内部字段也暴露给了 JSON 层。

数据库 NULL、Rows.Scan、sql.NullString 的 String 和 Valid 字段以及 JSON 编码边界的静态关系说明图
图1:结构说明图,展示数据库 NULL 到 sql.NullString 字段的静态映射,不是运行截图。

为什么 JSON 输出是对象而不是 null

问题出在 encoding/json 的职责边界:它看到的是一个普通结构体,而不是一个实现了“空值序列化”协议的特殊对象。sql.NullString 没有为 JSON 实现 MarshalJSON,因此两个导出字段都会参与编码。

type User struct {
    Nickname sql.NullString `json:"nickname"`
}

func encodeUser() ([]byte, error) {
    // Valid=false 只表达 SQL NULL 的状态,不会改变 Go 类型本身。
    user := User{Nickname: sql.NullString{String: "", Valid: false}}
    data, err := json.Marshal(user)
    if err != nil {
        // 返回编码错误,调用方可以决定是否终止响应。
        return nil, fmt.Errorf("marshal user: %w", err)
    }
    return data, nil
}

// 结果形态:{"nickname":{"String":"","Valid":false}}

这也是为什么给字段加上 omitempty 往往不能解决问题:字段的实际类型仍然是结构体,JSON 编码器不会根据其中的 Valid 字段猜测你的 API 语义。SQL NULL、Go 结构体和 JSON null 是三个不同层次的表示。

encoding/json 将 String 和 Valid 编码为 JSON 对象并与 *string、MarshalJSON 输出 JSON null 的静态关系说明图
图2:表示边界说明图,对比结构体对象、*string 与 JSON null 的静态关系,不是运行结果截图。

在接口边界转换成真正的 null

更适合长期维护的做法,是让数据库层继续使用 sql.NullString,在 API DTO 层明确决定对外表示。最简单的是转换成指针:非 NULL 时给出字符串指针,NULL 时保留 nil。

type UserResponse struct {
    Nickname *string `json:"nickname"`
}

func toUserResponse(user User) UserResponse {
    if !user.Nickname.Valid {
        // nil 会被 encoding/json 编码为 JSON null。
        return UserResponse{Nickname: nil}
    }

    value := user.Nickname.String
    // 取局部值的地址,避免把数据库承载类型直接泄漏到响应模型。
    return UserResponse{Nickname: &value}
}

// NULL 结果:{"nickname":null}
// 有值结果:{"nickname":"Ada"}

指针方案把 API 的语义写进了类型:nil 是没有值,非 nil 是有值。若项目已经大量使用 sql.NullString,也可以只为响应层定义一个包装类型:

type JSONNullString struct {
    sql.NullString
}

func (n JSONNullString) MarshalJSON() ([]byte, error) {
    if !n.Valid {
        // 明确返回 JSON 字面量 null,而不是带字段的对象。
        return []byte("null"), nil
    }
    // 有值时仍按 JSON 字符串规则处理引号和转义。
    return json.Marshal(n.String)
}

自定义类型适合需要统一复用的响应模型,但它也把序列化策略绑定到了这个类型上。不要为了“看起来像 null”直接把 SQL NULL 转成空字符串,否则客户端无法区分“用户没有填写”和“用户填写了空文本”。

数据库层与接口层怎样分工

场景推荐表示原因
扫描数据库列sql.NullString同时保留字符串值与 NULL 状态
接口允许 null*string 或自定义 MarshalJSON把对外 JSON 语义写清楚
接口约定空字符串显式转换为 string确认客户端确实不需要区分 NULL
兼容旧客户端保留旧字段,新增明确的可空字段避免突然改变响应形状

结论可以压缩成一句话:sql.NullString.Valid=false 表示数据库层的 NULL 状态,并不等于 Go 值本身是 nil;默认 JSON 编码看到的是结构体,所以输出对象。只有在响应边界显式使用 *string 或实现 MarshalJSON,才能稳定输出 JSON null。

常见问题

把 sql.NullString.String 直接返回可以吗?

可以,但会把 NULL 和空字符串都变成同一个空文本。只有业务明确接受这种信息丢失时才这样做。

给 NullString 字段加 json:",omitempty" 能输出 null 吗?

不能把它当作可靠方案。字段仍是结构体,encoding/json 不会读取 Valid 来替你改变编码类型;需要 null 时应在 DTO 层转换。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go database/sql Tx.Commit 为什么可能返回连接错误Go database/sql Tx.Commit 为什么可能返回连接错误
上一篇
Go database/sql Tx.Commit 为什么可能返回连接错误
Go database/sql Context 取消后为什么连接仍未释放
下一篇
Go database/sql Context 取消后为什么连接仍未释放
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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模型性能。
    717次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    724次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    800次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    760次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    593次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码