前言
處理傳遞資料時都快忘了有「序列化與反序列化資料」這個步驟,因為都被套件像是: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