Go 中正确序列化二维指针切片为 JSON 的完整指南

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 的导出规则,即可零成本、安全高效地序列化任意深度的指针结构。