Go 结构体有数据,json.Marshal 却返回空对象 {}
Go 结构体字段未导出时,JSON 标签无法让 encoding/json 编码这些字段。用导出字段配合小写标签,并排除省略规则、嵌入字段冲突和自定义编码方法。
打印结构体能看到字段内容,json.Marshal 也没有返回错误,但输出只有 {}。对于字段名全部小写的普通结构体,JSON 标签并不能让这些字段被编码:
type Profile struct {
name string `json:"name"`
email string `json:"email"`
}
本文针对标准库 encoding/json 的默认结构体编码,不包含自定义 MarshalJSON、第三方编码器或其他 JSON 包。{} 表示这里成功编码出了一个空对象,不等于编码过程报错。
标签决定 JSON 名称,不能替代字段导出
Go 的跨包可见性由标识符是否导出决定。普通结构体字段以大写字母开头才能导出,encoding/json 默认只把可参与编码的导出字段放进对象。
因此,name string 即使带有 json:"name",仍然是未导出字段。要保留小写的 JSON 键名,应把 Go 字段改成大写,再用标签指定对外名称:
package main
import (
"encoding/json"
"fmt"
)
type Profile struct {
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
profile := Profile{
Name: "demo-user",
Email: "user@example.com",
}
data, err := json.Marshal(profile)
if err != nil {
panic(err)
}
fmt.Println(string(data))
}
按示例规则,JSON 应包含 name 与 email 两个键及相应值。结构体名称是否导出,不能替代字段自身的导出条件;要检查的是字段声明,而不是只把类型名改大写。
更名时同步更新结构体字面量和所有字段访问。示例使用虚构邮箱,诊断真实对象时不要把密码、令牌等字段整体打印到日志。
字段已经大写时,检查省略规则
如果字段已经导出,继续核对标签与实际值:
type Response struct {
Name string `json:"name,omitempty"`
Secret string `json:"-"`
}
在默认编码下,Secret 无论有没有值都会被忽略;Name 是空字符串时会因为 omitempty 被省略。若一个对象的所有字段都符合省略条件,结果也可能是 {}。
标签选项要准确写成 json:"name,omitempty",不要在逗号后随意加空格。字段仍须满足导出条件,标签只控制编码方式。
对于嵌入结构体,还可能存在同层字段名称冲突:默认选择规则无法选出唯一字段时,相关字段会被忽略,而且不一定报错。出现这种结构时,应对照完整类型检查,不能把所有空对象都归因于小写字段。
如果类型实现了 MarshalJSON,返回内容可能由该方法控制;应先检查实际传给 Marshal 的类型及其方法,再判断默认规则是否适用。
只导出确实需要对外提供的字段
不要为了让 JSON 不再为空,就把内部结构体的所有字段统一改大写。对于只需要公开部分数据的接口,可以建立专用响应结构体:
type PublicProfile struct {
Name string `json:"name"`
}
response := PublicProfile{Name: internalProfile.name}
data, err := json.Marshal(response)
这个片段要求当前包内已经有 internalProfile,且随后按应用的错误处理方式检查 err。专用结构体把输出字段限定为 name,避免改动内部对象的可见性约定。
修复后的检查应包含“必需键存在、值正确、敏感键不存在”,并覆盖字段为空的情况。JSON 键名大小写也是接口契约的一部分;保留标签可以避免 Go 字段更名顺带改变客户端看到的键名。
回退时同时恢复字段声明和调用处,保留上一版响应样例供比较。已经发给外部系统的数据不会因代码回退而撤回,应先确认本次更名有没有意外增加输出字段。


