json.marshal将结构体序列化为空对象{},是因为go只序列化首字母大写的导出字段,小写私有字段(如name string)无论是否加json:"name"标签均被静默忽略;嵌套结构体也需逐层导出,否则整层消失。

为什么 json.Marshal 会把结构体字段序列化成空对象
因为 Go 的 json.Marshal 只能访问导出(首字母大写)字段,所有小写字母开头的私有字段会被直接跳过,不报错也不提示。你看到 {} 或部分字段缺失,大概率是结构体里混用了私有字段。
典型场景:从数据库读取数据后直接用自定义结构体接收,但字段名按习惯写成 id、name、createdAt 等小写形式;或者为了封装把字段设为私有,又没加 JSON 标签补救。
- 即使字段有默认值(如
int为 0、string为空),只要不导出,就不会出现在 JSON 中 -
json:"-"标签会显式忽略字段,但私有字段不需要这个标签也自动被忽略 - 嵌套结构体中,只要内层字段不导出,整层都“消失”,不会留空对象或 null
如何快速确认是不是字段未导出导致的问题
用 fmt.Printf("%+v", yourStruct) 打印结构体本身,看字段名是否全为小写;再对比 json.Marshal 输出,如果字段在 %+v 里存在、JSON 里却没了,基本就是导出问题。
更直接的办法:临时把字段名首字母大写,重新跑一次 json.Marshal,如果 JSON 立刻出现对应字段,就坐实了。
- 别依赖 IDE 的“重命名”功能自动改大小写——它可能漏掉嵌套字段或别名类型
- 注意
type UserID int这类类型别名,其底层字段仍需导出,类型别名本身不改变字段可见性 - 如果用了
json.RawMessage或自定义MarshalJSON方法,优先排查这些逻辑是否覆盖了默认行为
正确导出字段 + 控制 JSON 键名的写法
导出字段只是第一步,还要配合 json struct tag 控制输出键名。比如想保留小写 key(如 "user_id"),不能只靠改字段名为 UserID,还得加 tag:
type User struct {
UserID int `json:"user_id"`
UserName string `json:"user_name"`
isActive bool `json:"-"` // 私有字段,加不加 "-" 都不出现
}
注意:tag 中的 omitempty 不影响导出逻辑,只控制零值是否省略;而 json:""(空字符串)会让字段使用字段名原样(即大驼峰转小写下划线),但不如显式写清楚可靠。
- 字段名必须大写,否则 tag 完全无效
- 如果字段是接口类型(如
json.RawMessage或interface{}),确保赋值内容本身可序列化 - 嵌套结构体字段也要逐层检查导出状态,常见坑是外层导出、内层私有
不想改字段名时的替代方案
真不能改字段名(比如要兼容已有内存模型或 ORM 映射),唯一合规方式是实现 MarshalJSON 方法:
func (u User) MarshalJSON() ([]byte, error) {
type Alias User // 防止无限递归
return json.Marshal(struct {
*Alias
UserID int `json:"user_id"`
UserName string `json:"user_name"`
}{
Alias: (*Alias)(&u),
UserID: u.userID,
UserName: u.userName,
})
}
这方法有效,但代价高:每次序列化都新建匿名结构体、手动映射字段,且无法复用标准 tag 逻辑。大多数情况下,老老实实把字段改成大写 + 加 tag 更轻量、易维护。
真正容易被忽略的是:当结构体嵌套多层、或来自第三方包时,你可能根本没意识到某一层用了私有字段——这时候打印 reflect.TypeOf(yourStruct).NumField() 能快速看清哪些字段被反射认为“不可见”。











