
go 的 `json` 包可自动解引用结构体指针,但要求所有字段首字母大写(即导出),否则因反射无法访问而输出空对象 `{}`。本文详解如何正确配置结构体、避免常见陷阱,并提供可运行示例。
在 Go 中对包含指针的嵌套结构(如 [][]*cell)进行 JSON 序列化时,开发者常遇到 json.Marshal 返回空对象 {} 或 [] 的问题。根本原因并非指针本身不被支持——恰恰相反,标准库 encoding/json 完全支持指针解引用;真正的问题在于:Go 的 JSON 编码器依赖反射访问字段,而反射只能读取导出(首字母大写)的字段。
观察原始代码中的结构体定义:
type point struct {
x int // ❌ 小写 → 未导出 → JSON 忽略
y int // ❌ 小写 → 未导出 → JSON 忽略
}由于 x 和 y 是非导出字段,json.Marshal 在反射过程中完全不可见,导致整个 point 字段被跳过,最终生成空 JSON 对象(如 "Point":{} 或更糟——若嵌套更深可能直接省略)。
✅ 正确做法是将所有需序列化的字段改为导出字段(首字母大写),并保持指针语义不变:
type Point struct { // 建议类型名也导出(大写)
X int `json:"x"` // 可选:用 tag 控制 JSON 键名
Y int `json:"y"`
}
type Cell struct {
Point Point `json:"point"`
Visited bool `json:"visited"`
Walls Walls `json:"walls"`
}
type Walls struct {
N bool `json:"n"`
E bool `json:"e"`
S bool `json:"s"`
W bool `json:"w"`
}
type Maze struct {
Cells [][]*Cell `json:"cells"` // 二维指针切片,无需转换为值切片
}此时,即使 Cells 是 [][]*Cell 类型,json.Marshal 仍能正确遍历每个 *Cell,自动解引用并序列化其导出字段:
func main() {
m := Maze{}
row1 := []*Cell{{Point: Point{X: 1, Y: 2}, Visited: true, Walls: Walls{N: true}}}
row2 := []*Cell{{Point: Point{X: 3, Y: 4}, Visited: false, Walls: Walls{E: true}}}
m.Cells = [][]*Cell{row1, row2}
data, err := json.MarshalIndent(m, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(data))
}输出结果(格式化后):
{
"cells": [
[
{
"point": {
"x": 1,
"y": 2
},
"visited": true,
"walls": {
"n": true,
"e": false,
"s": false,
"w": false
}
}
],
[
{
"point": {
"x": 3,
"y": 4
},
"visited": false,
"walls": {
"n": false,
"e": true,
"s": false,
"w": false
}
}
]
]
}? 关键注意事项:
- ✅ 指针无需手动解引用或深拷贝:json 包原生支持 *T,只要 T 的字段可导出;
- ✅ 使用 json:"key" tag 可自定义 JSON 字段名,兼顾 Go 命名规范(CamelCase)与 JSON 习惯(snake_case);
- ⚠️ 若结构体含 nil 指针(如 []*Cell{nil, &c}),对应位置 JSON 会输出 null,符合预期;
- ⚠️ 避免循环引用(如 cell.parent *cell),否则 json.Marshal 将 panic;
- ? 如需更精细控制(如忽略零值、自定义序列化逻辑),可实现 json.Marshaler 接口。
综上,修复的核心不是“绕过指针”,而是“导出字段”。遵循 Go 的导出规则,即可零成本、安全高效地序列化任意深度的指针结构。









