本文 AI 產出,尚未審核

Golang – 結構與介面

主題:結構標籤(tags)與 JSON 處理


簡介

在 Go 語言的日常開發中,**結構(struct)是最常用的資料模型,而結構標籤(struct tags)**則是讓這些模型與外部系統(如 JSON、XML、資料庫)互動的關鍵橋樑。特別是當我們需要把 Go 物件序列化成 JSON,或是把收到的 JSON 反序列化回 Go 結構時,標籤的設定直接決定了欄位名稱、是否忽略、預設值等行為。

掌握結構標籤的寫法與常見陷阱,能讓 API 開發、微服務間的資料傳遞、以及前後端協作變得更安全、可維護且易於除錯。本文將從概念說明、實作範例、最佳實踐一路帶你深入了解 Go 的 struct tags,並結合 encoding/json 套件完成完整的 JSON 處理流程。


核心概念

1. 結構標籤的語法

在 Go 中,結構欄位的宣告可以在型別之後加上一對反引號(`),裡面放入以空白分隔的 key:"value" 形式的字串。最常見的 key 為 jsonxmldbvalidate 等。

type User struct {
    ID   int    `json:"id"`               // 直接映射成 JSON 的 "id"
    Name string `json:"name,omitempty"`   // 若 Name 為空字串則省略此欄位
    Age  int    `json:"-"`                // 完全忽略,不會出現在 JSON 中
}
  • key:標籤類型(此例為 json)。
  • value:對應的設定字串,可包含多個選項,以逗號分隔。
  • omitempty:若欄位值為零值(0、""、nil、false、空 slice/map),在序列化時會被省略。
  • "-":告訴編碼器此欄位永遠不會被編碼或解碼。

小技巧:標籤字串必須使用雙引號包住,且在同一個欄位可以同時放多個 key(例如 json:"id" db:"user_id"),用空白分隔即可。

2. JSON 序列化與反序列化

Go 標準庫的 encoding/json 提供兩個核心函式:

函式 功能 常見使用情境
json.Marshal(v interface{}) ([]byte, error) 把任意 Go 值編碼成 JSON 位元組 回傳 API、寫入檔案
json.Unmarshal(data []byte, v interface{}) error 把 JSON 位元組解碼到 Go 結構 接收前端請求、讀取設定檔

這兩個函式會自動根據結構標籤決定欄位名稱、是否忽略、omitempty 等行為。

3. 標籤與欄位可見性

只有 導出的欄位(首字母大寫)才會被 json 套件處理。即使標籤寫得再正確,若欄位是私有的(小寫開頭),編碼器仍會忽略。

type Private struct {
    secret string `json:"secret"` // 不會被編碼,因為 secret 是私有欄位
}

4. 嵌入式結構(Embedded Struct)與標籤繼承

嵌入式結構會把內部欄位提升(promote)到外層結構,標籤仍然有效。若外層與內層同名欄位衝突,外層會「遮蔽」內層。

type Base struct {
    ID   int `json:"id"`
    Time int64 `json:"timestamp"`
}

type Article struct {
    Base          // 嵌入
    Title string `json:"title"`
    // 若想保留 Base 的 Time 欄位名稱,可在外層重新定義
    // Timestamp int64 `json:"timestamp"` // 會覆寫 Base 的 Time
}

程式碼範例

以下示範 5 個常見且實用的情境,從最基礎的序列化到進階的自訂 Unmarshal。

範例 1:基本的 JSON 編碼與解碼

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name,omitempty"` // Name 為空時會被省略
    Age  int    `json:"age"`
    // 忽略密碼欄位
    Password string `json:"-"`
}

func main() {
    u := User{ID: 1, Name: "", Age: 30, Password: "secret"}

    // Marshal → JSON
    data, err := json.Marshal(u)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(data)) // {"id":1,"age":30}

    // Unmarshal → struct
    var u2 User
    if err := json.Unmarshal(data, &u2); err != nil {
        panic(err)
    }
    fmt.Printf("%+v\n", u2) // {ID:1 Name: Age:30 Password:}
}

說明

  • omitemptyName 為空時不出現在 JSON。
  • json:"-" 完全隱藏 Password

範例 2:自訂欄位名稱與多標籤

type Product struct {
    SKU      string  `json:"sku" db:"product_sku"`           // 同時支援 JSON 與資料庫
    Price    float64 `json:"price_cents,string"`            // 以字串形式編碼,避免精度問題
    InStock  bool    `json:"in_stock"`                      // 標準布林
    Category string  `json:"category,omitempty" xml:"cat"` // 同時支援 XML
}
  • json:"price_cents,string" 會把 float64 轉成字串,常用於金額避免浮點誤差。
  • 多標籤讓同一結構可同時用於不同序列化套件。

範例 3:嵌入式結構與欄位衝突解決

type BaseInfo struct {
    ID   int    `json:"id"`
    Time int64  `json:"timestamp"`
}

type Event struct {
    BaseInfo               // 嵌入
    ID   string `json:"event_id"` // 覆寫外層的 id 欄位名稱
    Name string `json:"name"`
}
func main() {
    e := Event{
        BaseInfo: BaseInfo{ID: 100, Time: 1620000000},
        ID:       "E-2023-001",
        Name:     "Launch Event",
    }
    b, _ := json.Marshal(e)
    fmt.Println(string(b))
    // {"event_id":"E-2023-001","name":"Launch Event","timestamp":1620000000}
}

重點:外層的 IDevent_id 替代了嵌入式的 id,避免欄位衝突。


範例 4:自訂 Unmarshal 以支援多種日期格式

type Order struct {
    OrderID   string    `json:"order_id"`
    CreatedAt CustomTime `json:"created_at"` // 使用自訂型別
}

// CustomTime 包裝 time.Time,實作 json.Unmarshaler
type CustomTime struct {
    time.Time
}

// 支援 RFC3339 與 Unix timestamp 兩種格式
func (ct *CustomTime) UnmarshalJSON(b []byte) error {
    // 去除雙引號
    s := strings.Trim(string(b), `"`)
    // 嘗試 RFC3339
    if t, err := time.Parse(time.RFC3339, s); err == nil {
        ct.Time = t
        return nil
    }
    // 嘗試 Unix 秒數
    if sec, err := strconv.ParseInt(s, 10, 64); err == nil {
        ct.Time = time.Unix(sec, 0)
        return nil
    }
    return fmt.Errorf("invalid time format: %s", s)
}
func main() {
    jsonStr1 := `{"order_id":"A001","created_at":"2023-10-01T15:04:05Z"}`
    jsonStr2 := `{"order_id":"A002","created_at":"1696165445"}` // Unix 秒

    var o1, o2 Order
    json.Unmarshal([]byte(jsonStr1), &o1)
    json.Unmarshal([]byte(jsonStr2), &o2)

    fmt.Println(o1.CreatedAt) // 2023-10-01 15:04:05 +0000 UTC
    fmt.Println(o2.CreatedAt) // 2023-10-01 15:04:05 +0000 UTC
}

說明:透過實作 json.Unmarshaler,我們可以讓同一欄位接受多種日期表示方式,提升 API 的彈性。


範例 5:使用 omitempty 與指標(pointer)避免零值被序列化

type Config struct {
    Host string  `json:"host"`               // 必填
    Port *int    `json:"port,omitempty"`      // 若為 nil 則不出現在 JSON
    TLS  *bool   `json:"tls_enabled,omitempty"` // 同上
}
func main() {
    port := 8080
    cfg := Config{
        Host: "example.com",
        Port: &port, // 會被序列化
        TLS:  nil,   // 省略
    }
    b, _ := json.Marshal(cfg)
    fmt.Println(string(b)) // {"host":"example.com","port":8080}
}

要點:使用指標可以區分「未設定」 (nil) 與「零值」 (0false),配合 omitempty 可精確控制輸出。


常見陷阱與最佳實踐

陷阱 可能的結果 建議的解決方案
欄位未導出(小寫開頭) json.Marshal 產生空物件或缺少欄位 確保所有需要序列化的欄位首字母大寫
標籤拼寫錯誤(如 jsno:"id" 編碼/解碼失敗,欄位名稱變成預設的欄位名 使用 IDE 或 go vet 檢查結構標籤
omitempty 與指標混用不當 零值被錯誤省略,導致 API 客戶端缺少必要欄位 若欄位必須出現,即使是零值,也不要加 omitempty;若要區分「未設定」與「零值」,使用指標
時間格式不一致 json.Unmarshal 失敗或得到錯誤的時間 為時間欄位實作自訂 Unmarshaler,或在 API 文檔明確規範時間格式
同名欄位衝突(嵌入式結構) 序列化結果不符合預期,可能被遮蔽 明確在外層重新定義欄位名稱或使用 json:"-" 隱藏不需要的欄位
使用 interface{} 失去類型安全 需要自行斷言類型,容易出錯 盡量使用具體結構或 map[string]json.RawMessage 進行分段解碼

最佳實踐

  1. 明確宣告 JSON 欄位名稱:即使欄位名稱與結構相同,也建議寫上 json:"field_name",避免未來重構時產生差異。
  2. 使用 omitempty 搭配指標:可在不影響 API 合約的前提下,減少傳輸的資料量。
  3. 集中管理標籤:若多個結構共用相同的 JSON 規則(如時間格式),可抽出共用型別或自訂 Marshal/Unmarshal。
  4. 加入單元測試:使用 encoding/json 的 round‑trip 測試(Marshal → Unmarshal → Compare)確保標籤正確。
  5. 使用 go vet -json:檢查結構標籤的語法錯誤與未導出的欄位。

實際應用場景

場景 為何需要結構標籤 典型實作方式
RESTful API 回傳 前端期望的欄位名稱與 Go 結構可能不同(例如 snake_case vs camelCase) 使用 json:"snake_case",搭配 omitempty 減少不必要欄位
微服務間訊息傳遞(Kafka / NATS) 訊息格式必須嚴格遵守合約,且常有版本升級需求 透過自訂型別與 Unmarshaler 處理多版本欄位,保持向前/向後相容
資料庫 ORM(GORM、sqlx) 同一結構同時對應資料庫欄位與 JSON 同時寫 json:"name" db:"user_name",保持單一來源
設定檔讀寫(JSON/YAML) 設定檔可能包含可選欄位,且需要支援預設值 使用指標 + omitempty,或在 UnmarshalJSON 中填入預設值
日誌結構化輸出 日誌系統(如 ELK)要求特定欄位名稱 在 log struct 中使用 json:"@timestamp"json:"level" 等特殊標籤

總結

結構標籤是 Go 與外部世界溝通的關鍵,尤其在 JSON 處理上扮演不可或缺的角色。透過正確的 json 標籤,我們可以:

  • 自訂欄位名稱,符合前端或第三方 API 的命名慣例。
  • 控制欄位的出現與隱藏omitempty"-"),減少不必要的資料傳輸。
  • 支援多種資料來源(資料庫、XML、驗證),只需在同一結構上掛多個標籤。
  • 處理特殊類型(時間、金額、指標),提升 API 的彈性與安全性。

在實務開發中,記得檢查欄位是否導出、標籤拼寫是否正確,以及適時使用自訂 Marshal/Unmarshal 來因應複雜需求。結合單元測試與 go vet,即可在開發早期捕捉大部分錯誤,讓你的 JSON API 更加 可靠、易維護且符合業務需求

祝你在 Golang 的結構與介面世界裡玩得開心,寫出乾淨、可讀、且高效的 JSON 程式碼!