前言
处理传递数据时都快忘了有「序列化与反序列化数据」这个步骤,因为都被像: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"`}延伸阅读
- What does Go’s JSON package mean when it refers to Marshal? [closed] - stack overflow
- What is the difference between Serialization and Marshaling? - stack overflow
- Go by Example: JSON
- Work with BSON - MongoDB