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 為 json、xml、db、validate 等。
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:}
}
說明:
omitempty讓Name為空時不出現在 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}
}
重點:外層的 ID 以 event_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) 與「零值」 (0、false),配合 omitempty 可精確控制輸出。
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 建議的解決方案 |
|---|---|---|
| 欄位未導出(小寫開頭) | json.Marshal 產生空物件或缺少欄位 |
確保所有需要序列化的欄位首字母大寫 |
標籤拼寫錯誤(如 jsno:"id") |
編碼/解碼失敗,欄位名稱變成預設的欄位名 | 使用 IDE 或 go vet 檢查結構標籤 |
| omitempty 與指標混用不當 | 零值被錯誤省略,導致 API 客戶端缺少必要欄位 | 若欄位必須出現,即使是零值,也不要加 omitempty;若要區分「未設定」與「零值」,使用指標 |
| 時間格式不一致 | json.Unmarshal 失敗或得到錯誤的時間 |
為時間欄位實作自訂 Unmarshaler,或在 API 文檔明確規範時間格式 |
| 同名欄位衝突(嵌入式結構) | 序列化結果不符合預期,可能被遮蔽 | 明確在外層重新定義欄位名稱或使用 json:"-" 隱藏不需要的欄位 |
使用 interface{} 失去類型安全 |
需要自行斷言類型,容易出錯 | 盡量使用具體結構或 map[string]json.RawMessage 進行分段解碼 |
最佳實踐
- 明確宣告 JSON 欄位名稱:即使欄位名稱與結構相同,也建議寫上
json:"field_name",避免未來重構時產生差異。 - 使用
omitempty搭配指標:可在不影響 API 合約的前提下,減少傳輸的資料量。 - 集中管理標籤:若多個結構共用相同的 JSON 規則(如時間格式),可抽出共用型別或自訂 Marshal/Unmarshal。
- 加入單元測試:使用
encoding/json的 round‑trip 測試(Marshal → Unmarshal → Compare)確保標籤正確。 - 使用
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 程式碼!