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"`
}

延伸閱讀