Go sql.NullString 为什么 JSON 输出不是 null
来源:17golang原创
时间:2026-10-26 19:40:29 237浏览 收藏
在接口联调时,数据库里的昵称明明是 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 层。

为什么 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 是三个不同层次的表示。

在接口边界转换成真正的 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 层转换。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
452 收藏
-
390 收藏
-
126 收藏
-
382 收藏
-
142 收藏
-
461 收藏
-
240 收藏
-
231 收藏
-
272 收藏
-
394 收藏
-
155 收藏
-
176 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习