
问题现象:json.Marshal返回空JSON对象
go语言的encoding/json包是处理json数据序列化和反序列化的核心工具。然而,开发者在使用json.marshal将go结构体转换为json字符串时,有时会遇到一个令人困惑的现象:函数返回的json字符串是一个空的{},但同时err变量却是nil,这表明序列化过程“没有错误”。更让人不解的是,原始的go结构体变量本身包含了完整的数据。
以下是一个典型的示例代码,展示了这种问题:
package main
import (
"encoding/json"
"fmt"
)
type Address struct {
street string // 未导出字段
extended string // 未导出字段
city string // 未导出字段
state string // 未导出字段
zip string // 未导出字段
}
type Name struct {
first string // 未导出字段
middle string // 未导出字段
last string // 未导出字段
}
type Person struct {
name Name // 嵌套结构体,字段未导出
age int // 未导出字段
address Address // 嵌套结构体,字段未导出
phone string // 未导出字段
}
func main() {
myname := Name{"Alfred", "H", "Eigenface"}
myaddr := Address{"42 Place Rd", "Unit 2i", "Placeton", "ST", "00921"}
me := Person{myname, 24, myaddr, "000 555-0001"}
b, err := json.Marshal(me)
if err != nil {
fmt.Println("序列化错误:", err)
}
fmt.Println("序列化结果:", string(b)) // 输出: 序列化结果: {}
fmt.Println("原始结构体:", me) // 输出: 原始结构体: { {Alfred H Eigenface} 24 {42 Place Rd Unit 2i Placeton ST 00921} 000 555-0001}
}从输出可以看到,string(b)打印出{},而me结构体却包含了所有数据。这表明问题并非出在数据缺失,而是序列化过程未能正确识别并处理这些数据。
根本原因:Go语言的字段可见性规则
Go语言中,结构体字段的可见性(或称为导出性)由其名称的首字母大小写决定。
- 首字母大写的字段是已导出(Exported)的,它们可以在包外部被访问,并且可以被encoding/json等标准库进行序列化。
- 首字母小写的字段是未导出(Unexported)的,它们只能在定义它们的包内部被访问。对于encoding/json包来说,这些未导出的字段是不可见的,因此它们不会被序列化到JSON输出中。
在上述示例代码中,Address、Name和Person结构体中的所有字段(如street, first, age等)都是以小写字母开头的,这意味着它们都是未导出字段。当json.Marshal尝试序列化Person结构体时,它发现没有任何可导出的字段,因此生成了一个空的JSON对象{}。
立即学习“go语言免费学习笔记(深入)”;
至于err为何为nil,这是因为json.Marshal函数在序列化过程中并没有遇到任何导致失败的错误(例如循环引用、不可序列化的类型等)。它只是成功地序列化了所有它“能看到”的字段——在本例中,即零个可导出字段。因此,从其自身操作的角度来看,序列化是成功的,没有产生错误。
解决方案:导出结构体字段
解决这个问题的关键在于遵循Go语言的可见性规则,将需要序列化到JSON的结构体字段的首字母改为大写,使其成为可导出字段。
package main
import (
"encoding/json"
"fmt"
)
// Address 结构体,字段已导出
type Address struct {
Street string `json:"street"` // 使用json tag自定义JSON字段名
Extended string `json:"extended"`
City string `json:"city"`
State string `json:"state"`
Zip string `json:"zip"`
}
// Name 结构体,字段已导出
type Name struct {
First string `json:"first"`
Middle string `json:"middle,omitempty"` // omitempty 标签表示如果字段为空值则不序列化
Last string `json:"last"`
}
// Person 结构体,字段已导出
type Person struct {
Name Name `json:"name"`
Age int `json:"age"`
Address Address `json:"address"`
Phone string `json:"phone"`
}
func main() {
myname := Name{"Alfred", "H", "Eigenface"}
myaddr := Address{"42 Place Rd", "Unit 2i", "Placeton", "ST", "00921"}
me := Person{myname, 24, myaddr, "000 555-0001"}
b, err := json.Marshal(me)
if err != nil {
fmt.Println("序列化错误:", err)
return
}
fmt.Println("序列化结果:", string(b))
// 预期输出: {"name":{"first":"Alfred","middle":"H","last":"Eigenface"},"age":24,"address":{"street":"42 Place Rd","extended":"Unit 2i","city":"Placeton","state":"ST","zip":"00921"},"phone":"000 555-0001"}
}通过将street改为Street,first改为First等,这些字段现在都已导出,json.Marshal函数将能够正确地访问并序列化它们。在上面的示例中,我还额外添加了JSON标签(json:"..."),这是一种常见的做法,用于自定义JSON输出中的字段名称,使其符合常见的camelCase或snake_case约定,同时保持Go结构体字段的首字母大写以满足导出要求。例如,json:"middle,omitempty"表示在JSON中该字段名为middle,且当其为空值时将被省略。
注意事项
- 可见性是核心: 始终牢记Go语言的可见性规则是JSON序列化成功的基础。只有已导出的字段才会被encoding/json包处理。
-
json标签的灵活运用:
- json:"fieldName":自定义JSON输出中的字段名。这允许Go结构体字段保持大写以导出,而在JSON中则可以采用小写驼峰(camelCase)或下划线(snake_case)等常见命名约定。
- json:"-":忽略此字段,不进行序列化或反序列化。
- json:"fieldName,omitempty":如果字段为空值(例如,字符串为空、整数为0、切片为nil等),则在JSON输出中省略此字段。
- err返回nil的含义: 在此特定场景下,err为nil并不意味着得到了“预期”的JSON输出,它仅仅表示序列化过程本身没有发生运行时错误。对于空JSON对象的情况,需要检查结构体字段的可见性。
- 嵌套结构体: 嵌套结构体中的字段也必须是可导出的,json.Marshal才能正确地深入序列化。
- 官方文档: 遇到问题时,查阅encoding/json包的官方文档(go.dev/pkg/encoding/json)是获取最准确和最新信息的重要途径。
总结
Go语言中json.Marshal返回空JSON对象{}的问题,几乎总是由于结构体字段未导出(即首字母小写)造成的。理解并正确应用Go的可见性规则,确保所有需要序列化的字段都以大写字母开头,是解决此问题的根本方法。同时,合理利用json标签可以更精细地控制JSON输出格式。掌握这些基本原则,将有助于开发者高效、准确地处理Go语言中的JSON数据序列化任务。










