What is Marshal in Go?

Go 进行数据序列化与 Marshal 名称的起源

前言

处理传递数据时都快忘了有「序列化与反序列化数据」这个步骤,因为都被像:Axios🔗 这样的库抽象掉了,近期在写后端也重新温习相关知识,也延续先前文章:Go Struct Tag 是什么?如何透过 reflect 动态处理栏位? 探讨 Go 如何处理序列化数据。

fetch("x", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
})
// Axios 自动序列化资料
axios.post("x", {
email: "user@example.com",
project_id: "<project_id>"
})

序列化与编组

什么是序列化 Serialization?

程式语言有自己的资料结构,像是 Go 的 map 与 JavaScript 的 object 虽然外表相似但背后实践却完全不同,为了让两者相互沟通最常见就是透过 JSON 资料格式来传递资料。

把记忆体中的资料结构转换成可储存或可传输的格式称为序列化(Serialization),再将其还原回原始资料结构称为反序列化(Deserialization)。

什么是编组 Marshal

Marshal 原意是「集结」、「编排」或「整理」。

  • 军事: “To marshal troops”(集结部队),代表将零散的士兵排列成有序的阵型,以便行动。
  • 法律或活动: “To marshal facts”(整理事实),代表将混乱的资讯整理成有逻辑的结构。
  • 电脑科学:被借喻为将记忆体中「散落」或「指标导向」的复杂资料结构(如一个含有指标的 Struct),整队排列成一串可以传输的扁平(Flat)位元组。

Marshal 与 Serialization 的微小差异

虽然大多数情况下它们可以互换,但在电脑科学的术语定义上,两者存在细微差别:

特性Serialization (序列化)Marshalling (编组)
核心意图将资料转换为持久化格式(存档、存资料库)将资料转换为传输格式(跨程序或跨网路通讯)
包含内容通常只关注资料数值本身除了资料,有时还包含元数据(Metadata),甚至涉及远端过程调用(RPC)的处理
结构复杂度侧重于扁平化资料侧重于将复杂、非连续的物件「整队」成可传输的形式

Go Marshal

Go 标准库已经内建 encoding/json 套件。

  • Struct 栏位名称会直接成为 JSON key
  • 大写开头的栏位才能被序列化(exported)

encoding/json struct tag 基础用法

通常会透过 struct tag 来控制输出格式:

import (
"encoding/json"
"fmt"
)
func main() {
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
u := User{
Name: "Riceball",
Email: "rice@example.com",
}
b, _ := json.Marshal(u)
// 转换出 JSON 格式:{"name":"Riceball","email":"rice@example.com"}
fmt.Println(string(b))
}

常用的 tag 选项

type User struct {
ID int `json:"id"` // 重新命名栏位
Name string `json:"name,omitempty"` // 零值时不输出
Token string `json:"-"` // 完全忽略此栏位
}

Unmarshal 反序列化范例

type User struct {
ID int `json:"id"`
Name string `json:"name,omitempty"`
Email string `json:"email"`
Token string `json:"-"`
}
var u User
// 注意:JSON 中多余的栏位会被忽略
err := json.Unmarshal([]byte(`{"id":1,"name":"Riceball","email":"rice@example.com","extra":"ignored"}`), &u)
if err != nil {
fmt.Println("出错了:", err)
}
// 解析结果: {ID:1 Name:Riceball Email:rice@example.com Token:}
fmt.Printf("解析结果: %+v\n", u)

JSON Struct Tag 进阶用法

string 选项:强制转换为字串

当需要将数字或布林值在 JSON 中以字串形式呈现时,可以使用 string 选项:

  • 与某些旧系统或 API 对接时,可能要求所有数值都用字串传递
  • JavaScript 处理大数字时可能失去精度,用字串可以避免此问题
type Product struct {
ID int `json:"id,string"` // 输出为 "123" 而非 123
Price float64 `json:"price,string"` // 输出为 "99.99" 而非 99.99
InStock bool `json:"in_stock,string"` // 输出为 "true" 而非 true
}
p := Product{
ID: 123,
Price: 99.99,
InStock: true,
}
b, _ := json.Marshal(p)
fmt.Println(string(b))
// 输出: {"id":"123","price":"99.99","in_stock":"true"}

匿名嵌入:扁平化巢状结构

type Address struct {
City string `json:"city"`
Country string `json:"country"`
}
type User struct {
Name string `json:"name"`
Address // 匿名嵌入,栏位会被「提升」到 User 层级
}
u := User{
Name: "Riceball",
Address: Address{
City: "Taipei",
Country: "Taiwan",
},
}
b, _ := json.Marshal(u)
fmt.Println(string(b))
// 输出: {"name":"Riceball","city":"Taipei","country":"Taiwan"}
// 注意: Address 的栏位直接出现在最外层,而非巢状在 "address" 下

如果想要巢状结构,需明确命名:

type User struct {
Name string `json:"name"`
Address Address `json:"address"`
}
// 输出: {"name":"Riceball","address":{"city":"Taipei","country":"Taiwan"}}

Mongo Driver

import "go.mongodb.org/mongo-driver/bson"
type Address struct {
City string `bson:"city"`
Country string `bson:"country"`
}
type User struct {
Name string `bson:"name"`
Address Address `bson:",inline"` // MongoDB 的 inline 语法
}
// 存入 MongoDB 后的文件结构:
// {
// "name": "Riceball",
// "city": "Taipei", // 注意:不是巢狀在 address 下
// "country": "Taiwan"
// }

组合使用多个选项

type APIResponse struct {
Code int `json:"code,string,omitempty"`
Message string `json:"message,omitempty"`
Data any `json:"data,omitempty"`
}

延伸阅读