json.Marshal 为什么忽略未导出的字段
来源:17golang原创
时间:2026-11-13 09:42:45 295浏览 收藏
如果一个 Go 结构体交给 json.Marshal 后少了字段,先看字段名的首字母,而不是先怀疑 JSON 标签。默认规则很明确:首字母大写的字段是导出的,可以被 encoding/json 反射读取;首字母小写的字段未导出,默认序列化会忽略它。
json:"..."只改变 JSON 名称和空值策略,不能让未导出字段参与默认序列化。- 只想调整接口形状时优先使用 DTO;确实需要由类型自己决定 JSON 时再实现
MarshalJSON。 - 排查时同时看字段可见性、嵌套结构、
omitempty和实际传入的值,避免把空值误判成“字段被忽略”。

先用一个最小例子确认问题
下面的结构体同时放入一个导出字段 Name 和一个未导出字段 age。为了让输出结果容易观察,示例还给 Name 加上了 JSON 标签。
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `json:"name"` // 大写首字母表示导出,标签把 JSON 键改成 name
age int `json:"age"` // 小写首字母表示未导出,默认 Marshal 不会读取它
}
func main() {
user := User{Name: "Alice", age: 20}
b, err := json.Marshal(user)
if err != nil {
panic(err) // 示例直接终止;业务代码应按上下文返回或记录错误
}
fmt.Println(string(b)) // 输出:{"name":"Alice"}
}
这里最容易误会的地方是 age 明明写了 json:"age",结果仍然不出现。标签需要建立在“字段已经可以被编码器访问”的前提上;它能改名、设置 omitempty 或使用 - 排除字段,但不能越过 Go 的字段可见性规则。
关键差异:字段可见性优先于 JSON 标签
Go 语言规范把“名称首字母为 Unicode 大写字母,并且是包级声明、字段名或方法名”定义为导出标识符。encoding/json 的默认结构体编码会按照这套可见性规则选择字段。
| 写法 | 默认结果 | 适合场景 |
|---|---|---|
Name string | 参与序列化 | 字段本身就是稳定的外部数据契约 |
Name string `json:"name"` | 参与序列化,键名变为 name | Go 命名与接口命名不同 |
name string `json:"name"` | 仍然不参与默认序列化 | 内部状态不应直接暴露 |
Name string `json:"-"` | 明确排除 | 字段可导出,但不属于当前 JSON 契约 |
因此,看到“标签存在但字段缺失”时,第一检查项应该是字段名,而不是继续尝试更换标签拼写。
想保留私有字段时,优先做 DTO 投影
很多业务类型把数据库主键、缓存值或计算状态放在未导出字段中,这是合理的封装。此时不要为了让 json.Marshal 看到字段而破坏封装,可以定义一个只面向 JSON 的 DTO,把允许输出的值显式投影出去。
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int
name string // 内部字段保持未导出,外部包不能直接改写
}
type UserDTO struct {
ID int `json:"id"`
Name string `json:"name"` // DTO 只暴露 API 需要的字段
}
func (u User) ToDTO() UserDTO {
return UserDTO{ID: u.ID, Name: u.name} // 在边界处完成一次显式投影
}
func main() {
u := User{ID: 7, name: "Alice"}
b, err := json.Marshal(u.ToDTO())
if err != nil {
panic(err) // 生产代码可将错误交给 HTTP 层统一处理
}
fmt.Println(string(b)) // 输出:{"id":7,"name":"Alice"}
}
DTO 的好处是接口字段一目了然,内部模型变化也不会自动泄漏到 API。它适合大多数 HTTP 返回值、消息体和跨模块数据交换,是比“把所有字段都改成大写”更稳妥的修复。
什么时候应该实现 MarshalJSON
如果序列化规则确实属于类型本身,例如字段需要组合、脱敏或根据状态动态生成,类型可以实现 json.Marshaler。实现时要避免在 MarshalJSON 中再次直接调用 json.Marshal(u),否则会递归调用自己。
type User struct {
ID int
name string
}
func (u User) MarshalJSON() ([]byte, error) {
// 使用匿名辅助结构体,绕开对 User 自身的再次 Marshal,避免无限递归
return json.Marshal(struct {
ID int `json:"id"`
Name string `json:"name"`
}{
ID: u.ID,
Name: u.name,
})
}
这种方式的控制力更强,但也把序列化逻辑绑定到了类型上。若同一个内部类型要服务多个 API,DTO 通常更容易维护;若所有调用者都应遵循同一套编码规则,再考虑自定义方法。

几个看起来像“被忽略”的边界
omitempty 是空值策略,不是可见性开关
导出字段使用 omitempty 后,零值可能被省略。例如空字符串、数值零、false、空指针和空切片都可能让字段消失。先去掉标签中的 omitempty 观察一次,再判断是否真的存在可见性问题。
嵌套结构仍然逐层遵守导出规则
外层字段导出,并不意味着内部结构的所有字段都能输出。嵌套类型内部仍要检查字段首字母;匿名嵌入字段还会受到字段冲突和标签选择规则影响。
指针为 nil 与字段不存在不是一回事
指针字段没有 omitempty 时通常会输出 null;加上 omitempty 后可能完全不出现。这个结果来自空值策略,与未导出字段被默认忽略是两条不同的路径。
按问题选择最小方案
- 字段就是公共契约:改为导出字段,再用 JSON 标签控制名称。
- 内部模型不想暴露:创建 DTO,在转换函数中明确列出需要返回的数据。
- 所有调用者都必须共享特殊编码规则:实现
MarshalJSON,并用辅助结构体避免递归。 - 只是偶尔临时拼一个响应:可以使用匿名结构体或 map,但要留意类型约束和字段拼写。
常见问题
为什么字段加了 json 标签还是没有输出?
最常见原因是字段未导出,也就是名称首字母小写。先确认字段名,再决定改成导出字段、DTO 或自定义序列化。
把字段改成大写后,标签还需要吗?
不一定。没有标签时会使用 Go 字段名;如果接口要求小驼峰、固定缩写或需要 omitempty,仍然应该保留标签。
能不能用反射强行读取未导出字段?
不建议把绕过封装作为 JSON 接口方案。显式 DTO 或 MarshalJSON 更容易表达数据边界,也更不容易随着内部结构变化而产生意外输出。
自定义 MarshalJSON 为什么会无限递归?
如果方法内部再次对同一个类型调用 json.Marshal,编码器会重新进入该方法。使用匿名辅助结构体,或定义一个不带方法的别名类型,都可以切断递归。
记住这一条排查顺序就够了:先看字段是否导出,再看 JSON 标签,最后检查 omitempty、嵌套和指针状态。这样处理“json.Marshal 少字段”的问题,通常不需要更换库或修改反射代码。
-
377 收藏
-
125 收藏
-
201 收藏
-
183 收藏
-
440 收藏
-
196 收藏
-
307 收藏
-
353 收藏
-
308 收藏
-
222 收藏
-
439 收藏
-
298 收藏
-
Golang · Go问答 | 35分钟前 | Go问答 · 接口排查 · encoding/json · JSON Go unmarshal encoding/json DisallowUnknownFields UnmarshalTypeError struct tag273 收藏
-
Golang · Go问答 | 39分钟前 | net/http · Go问答 · Go net/http http/2 ResponseController ResponseWriter Hijack115 收藏
-
368 收藏
-
Golang · Go问答 | 42分钟前 | 重定向 · net/http · HTTP客户端 · Go问答 · Go HTTP重定向 http.Client CheckRedirect 跨主机跳转445 收藏
-
Golang · Go问答 | 1小时前 | net/http · Go问答 · 接口排查 · cookie Go Cookiejar http.Client Set-Cookie AddCookie116 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习