标题:Go 语言中将嵌套结构体高效转换为扁平数组的实践教程

本文介绍如何通过实现 `json.marshaler` 接口,将含嵌入结构体(如 `model`)的 go 结构体(如 `user`)序列化为紧凑、有序的 json 数组格式,适用于前端表格渲染等场景。

在构建 Web API 或数据导出服务时,有时需要将结构化数据以扁平数组形式(而非默认对象映射)输出,尤其当前端使用轻量级表格库(如 DataTables、Handsontable)并依赖固定字段顺序时。Go 默认的 JSON 序列化会生成键值对对象,而本文提供一种类型安全、可维护、高性能的替代方案:通过自定义 MarshalJSON() 方法,显式控制结构体到数组的映射逻辑。

✅ 核心思路:实现 json.Marshaler

Go 的 encoding/json 包允许任何类型通过实现 MarshalJSON() ([]byte, error) 方法,完全接管其 JSON 序列化行为。对于嵌入结构体(如 Model),我们可在 User.MarshalJSON() 中手动提取字段,并按预定义顺序拼装 []interface{},再交由 json.Marshal 编码。

以下是一个完整、可运行的示例:

package main

import (
    "encoding/json"
    "fmt"
    "time"
)

// 假设 bson.ObjectId 已被简化为 string(实际项目中请替换为真实类型)
type ObjectId string

func (ObjectId) Hex() string { return "507f1f77bcf86cd799439011" }

type Model struct {
    Id        ObjectId  `bson:"_id,omitempty"`
    CreatedAt time.Time `bson:",omitempty"`
    UpdatedAt time.Time `bson:",omitempty"`
    DeletedAt time.Time `bson:",omitempty"`
    CreatedBy ObjectId  `bson:",omitempty"`
    UpdatedBy ObjectId  `bson:",omitempty"`
    DeletedBy ObjectId  `bson:",omitempty"`
    Logs      []ObjectId `bson:",omitempty"`
}

type User struct {
    Name  string `bson:"name"`
    Model `bson:",inline"`
}

// MarshalJSON 实现:将 User 序列化为固定顺序的 JSON 数组
// 顺序:Name, Id, CreatedAt, UpdatedAt, DeletedAt, CreatedBy, UpdatedBy, DeletedBy, Logs
func (u User) MarshalJSON() ([]byte, error) {
    // 注意:Logs 是 []ObjectId,若需转为字符串切片可额外处理(如 u.LogsHex())
    arr := []interface{}{
        u.Name,
        u.Id.Hex(),                 // 转为字符串表示
        u.CreatedAt.Format(time.RFC3339),
        u.UpdatedAt.Format(time.RFC3339),
        u.DeletedAt.Format(time.RFC3339),
        u.CreatedBy.Hex(),
        u.UpdatedBy.Hex(),
        u.DeletedBy.Hex(),
        u.Logs, // 保持为数组(可选:转为 []string)
    }
    return json.Marshal(arr)
}

func main() {
    user := User{
        Name: "kiz",
        Model: Model{
            Id:        ObjectId("507f1f77bcf86cd799439011"),
            CreatedAt: time.Date(2014, 1, 1, 0, 0, 0, 0, time.UTC),
            UpdatedAt: time.Date(2014, 1, 1, 0, 0, 0, 0, time.UTC),
            DeletedAt: time.Date(2014, 1, 1, 0, 0, 0, 0, time.UTC),
            CreatedBy: ObjectId("507f1f77bcf86cd799439012"),
            UpdatedBy: ObjectId("507f1f77bcf86cd799439013"),
            DeletedBy: ObjectId("507f1f77bcf86cd799439014"),
            Logs:      []ObjectId{"507f1f77bcf86cd799439015"},
        },
    }

    data, _ := json.Marshal(map[string]interface{}{
        "rows": []User{user, user}, // 多个用户组成 rows 数组
    })
    fmt.Println(string(data))
    // 输出:
    // {"rows":[["kiz","507f1f77bcf86cd799439011","2014-01-01T00:00:00Z","2014-01-01T00:00:00Z","2014-01-01T00:00:00Z","507f1f77bcf86cd799439012","507f1f77bcf86cd799439013","507f1f77bcf86cd799439014",["507f1f77bcf86cd799439015"]],["kiz","507f1f77bcf86cd799439011","2014-01-01T00:00:00Z","2014-01-01T00:00:00Z","2014-01-01T00:00:00Z","507f1f77bcf86cd799439012","507f1f77bcf86cd799439013","507f1f77bcf86cd799439014",["507f1f77bcf86cd799439015"]]]}
}

⚠️ 注意事项与最佳实践

  • 字段顺序即契约:数组下标严格对应业务含义(如 row[0] 永远是 Name),前端必须与后端约定一致,建议配合常量定义索引(如 const NameIdx = 0)。
  • 嵌入结构体需显式展开:json.Marshaler 不自动递归处理嵌入字段,必须在 MarshalJSON() 中手动访问 u.Id、u.CreatedAt 等 —— 这反而是优势:完全可控、无反射开销。
  • 时间与 ID 格式化:原始 time.Time 和 bson.ObjectId 无法直接 JSON 序列化,务必提前调用 .Format() 或 .Hex() 转为字符串。
  • 反向解析(Unmarshal):如需从数组还原结构体,应同时实现 UnmarshalJSON([]byte) error,使用 json.Unmarshal 解析为 []interface{} 后,按序赋值(注意类型断言和指针接收器)。
  • 性能考量:相比反射方案(如 s2a 函数),此方法零反射、编译期确定、内存分配可控,适合高频 API 场景。

✅ 总结

无需复杂反射或泛型(Go 1.18+ 亦可扩展为泛型版本),仅通过实现 json.Marshaler 接口,即可优雅、高效地将含嵌入结构体的 Go 类型序列化为前端友好的扁平数组。它语义清晰、调试简单、性能优异,是面向特定消费方(如 JS 表格组件)输出数据的理想选择。