
本文介绍如何通过实现 `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 表格组件)输出数据的理想选择。










