本文 AI 產出,尚未審核

Golang – 結構與介面

單元:結構(struct)的定義與初始化


簡介

在 Go 語言中,**結構(struct)**是最常用的自訂資料型別之一。它讓我們能夠把多個相關的欄位(field)聚合在一起,形成一個有意義的實體,例如「使用者」、「商品」或「座標」等。掌握 struct 的定義與初始化,不僅是寫出可讀、可維護程式碼的基礎,也為後續的介面(interface)與方法(method)設計鋪路。

本篇文章將從 struct 的基本語法多種初始化方式常見陷阱 以及 實務應用 逐層說明,幫助初學者快速上手,同時提供給中級開發者一些最佳實踐的參考。


核心概念

1. struct 的基本語法

在 Go 中,使用 type 關鍵字搭配 struct 來宣告新型別。每個欄位都必須指定名稱與資料型別,欄位的順序會影響記憶體排列(memory layout),但在大多數情況下,我們只需要關注語意即可。

type Person struct {
    Name string   // 姓名
    Age  int      // 年齡
    Email string  // 電子郵件
}
  • 匿名欄位(embedding):若在 struct 中直接寫型別而不寫欄位名稱,會產生匿名欄位,等同於繼承(embedding)。
  • 欄位標籤(tag):常用於 JSON、XML 等序列化工具,例如 json:"name"
type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

2. 結構的初始化方式

Go 提供了多種初始化 struct 的手段,選擇哪一種取決於可讀性、效能與需求。以下示範四種常見方式:

2.1. 零值(Zero Value)

直接宣告變數,未指定欄位時會得到每個欄位的零值(string 為 ""、int 為 0、指標為 nil)。

var p Person          // 所有欄位皆為零值
fmt.Printf("%+v\n", p) // 輸出: {Name: Age:0 Email:}

2.2. 使用字面量(Composite Literal)

最直觀的方式,依照欄位順序或欄位名稱給值。

// 依照欄位順序
p1 := Person{"Alice", 30, "alice@example.com"}

// 使用欄位名稱(建議寫法,避免因欄位順序變動而出錯)
p2 := Person{
    Name:  "Bob",
    Age:   25,
    Email: "bob@example.org",
}

2.3. new 與指標

new(T) 會回傳指向 T 零值的指標,適合需要傳遞指標的情境。

pPtr := new(Person)          // *Person,所有欄位為零值
pPtr.Name = "Charlie"
pPtr.Age = 28
fmt.Printf("%+v\n", *pPtr)   // 輸出: {Name:Charlie Age:28 Email:}

2.4. 使用函式返回 struct

將 struct 包裝在工廠函式(factory function)中,能集中初始化邏輯,尤其當需要驗證或預設值時。

func NewPerson(name string, age int, email string) Person {
    if age < 0 {
        age = 0 // 防止負年齡
    }
    return Person{
        Name:  name,
        Age:   age,
        Email: email,
    }
}

p3 := NewPerson("Diana", -5, "diana@demo.com")
fmt.Printf("%+v\n", p3) // {Name:Diana Age:0 Email:diana@demo.com}

3. 結構與指標的差異

在函式參數傳遞、切片(slice)或 map 中使用 struct 時,指標可以避免不必要的複製,提升效能。

func UpdateAge(p *Person, newAge int) {
    p.Age = newAge
}

p := Person{Name: "Eve", Age: 22}
UpdateAge(&p, 23)
fmt.Println(p.Age) // 23

若直接傳遞 Person(非指標),函式內部會得到一個副本,原始變數不會被改變。

4. 結構的比較與相等性

Go 允許使用 == 比較兩個 struct,只要所有可比較的欄位都相等。若 struct 中含有 slice、map、func 等不可比較的欄位,則無法直接比較。

type Point struct {
    X, Y int
}

pA := Point{X: 1, Y: 2}
pB := Point{X: 1, Y: 2}
fmt.Println(pA == pB) // true

程式碼範例

下面提供 5 個實務中常見的範例,涵蓋從基本宣告到進階使用的情境。

範例 1:使用 JSON 標籤的 struct

type Article struct {
    ID      int    `json:"id"`
    Title   string `json:"title"`
    Content string `json:"content,omitempty"` // 空值時省略
}

func main() {
    a := Article{ID: 101, Title: "Go Struct 教學"}
    data, _ := json.Marshal(a)
    fmt.Println(string(data)) // {"id":101,"title":"Go Struct 教學"}
}

重點omitempty 可以讓輸出更精簡,避免傳送不必要的欄位。

範例 2:匿名欄位(Embedding)實作繼承

type Address struct {
    City    string
    ZipCode string
}

type Customer struct {
    Name    string
    Address // 匿名欄位,直接繼承 Address 的欄位
}

func main() {
    c := Customer{
        Name: "Frank",
        Address: Address{
            City:    "Taipei",
            ZipCode: "100",
        },
    }
    fmt.Printf("%+v\n", c) // {Name:Frank Address:{City:Taipei ZipCode:100}}
    // 直接存取繼承欄位
    fmt.Println(c.City) // Taipei
}

技巧:匿名欄位讓 struct 具備「組合」的特性,常用於建立共用屬性。

範例 3:工廠函式加上驗證

type Product struct {
    SKU   string
    Price float64
}

// NewProduct 會檢查價格是否為正數,若不合法則回傳錯誤
func NewProduct(sku string, price float64) (Product, error) {
    if price < 0 {
        return Product{}, fmt.Errorf("price cannot be negative")
    }
    return Product{SKU: sku, Price: price}, nil
}

func main() {
    p, err := NewProduct("ABC-123", -9.99)
    if err != nil {
        fmt.Println("建立失敗:", err) // 建立失敗: price cannot be negative
        return
    }
    fmt.Printf("%+v\n", p)
}

最佳實踐:把「資料驗證」集中在建構函式中,避免散落在程式各處。

範例 4:使用指標避免大量複製

type LargeData struct {
    ID   int
    Buf  [1024]byte // 大量資料
}

// 處理大量資料的函式,使用指標傳遞
func Process(ld *LargeData) {
    // 直接修改原始結構
    ld.ID = ld.ID + 1
}

func main() {
    data := LargeData{ID: 1}
    Process(&data)
    fmt.Println(data.ID) // 2
}

效能提醒:當 struct 含有大量欄位或陣列時,傳遞指標可減少記憶體拷貝。

範例 5:在 slice 中使用 struct

type Task struct {
    Title string
    Done  bool
}

func main() {
    tasks := []Task{
        {Title: "寫教學文章", Done: false},
        {Title: "測試程式碼", Done: true},
    }

    // 更新第二筆任務的狀態
    tasks[0].Done = true
    fmt.Printf("%+v\n", tasks)
}

實務觀點:slice 本身是引用型別,直接操作裡面的 struct 會改變底層資料,不需要額外的指標。


常見陷阱與最佳實踐

陷阱 說明 建議的做法
欄位順序變動導致字面量錯位 使用 Person{"Alice", 30, "a@b.com"} 時,若之後新增欄位會導致值對錯位。 永遠使用欄位名稱的字面量 (Person{Name:"Alice", Age:30, Email:"a@b.com"})。
忘記初始化指標欄位 struct 中若有指標欄位(如 *time.Time),未初始化會是 nil,使用前若直接解引用會 panic。 在建構函式或 New...預先分配,或在使用前檢查 nil
比較不可比較的 struct 包含 slice、map、func 的 struct 不能使用 == 若需要比較,可自行實作 Equal 方法或使用 reflect.DeepEqual(注意效能)。
過度使用全域 struct 變數 全域變數會造成資料競爭(race condition)與測試困難。 儘量 以參數傳遞或依賴注入 的方式取得 struct 實例。
忘記使用 JSON 標籤 直接 Marshal 時欄位名稱會變成大寫,與前端約定不符。 為所有對外輸出的 struct 加上 json:"field_name" 標籤。

最佳實踐小結

  1. 欄位名稱字面量:可讀性最高,防止未來欄位變更造成錯誤。
  2. 工廠函式:集中驗證與預設值,提升程式的安全性。
  3. 指標 vs 值:根據資料大小與是否需要修改決定傳遞方式。
  4. 使用標籤:與外部系統(JSON、DB、ORM)互動時,標籤是必備工具。
  5. 保持不可變性:若 struct 只作為資料傳輸(DTO),盡量使用值傳遞,避免意外修改。

實際應用場景

場景 為何使用 struct 範例
REST API 請求/回應 定義資料模型、加上 JSON 標籤,方便序列化與驗證。 type UserResponse struct { ID int \json:"id"` Name string `json:"name"` }`
資料庫 ORM 透過 struct 標籤映射資料表欄位,簡化 CRUD 操作。 type Order struct { OrderID int \gorm:"primaryKey"` Amount float64 `gorm:"column:total_amount"` }`
設定檔載入 把 YAML/JSON 設定檔映射成 struct,讓程式碼取得型別安全的設定值。 type Config struct { Port int \yaml:"port"` Debug bool `yaml:"debug"` }`
事件驅動系統 定義事件資料結構,讓不同模組共享同一個事件模型。 type Event struct { Type string Payload interface{} }
圖形與座標系 使用 struct 表示點、向量或矩形,配合方法實作運算。 type Vec2 struct { X, Y float64 }

總結

  • struct 是 Go 中最核心的自訂型別,提供了聚合相關欄位的能力。
  • 透過 字面量、new、指標與工廠函式 等多樣化的初始化方式,我們可以依需求選擇最合適的寫法。
  • 在實務開發中,欄位名稱字面量、標籤、指標使用 以及 集中驗證的工廠函式 是提升程式可讀性、可靠性與效能的關鍵。
  • 了解常見陷阱(如欄位順序、不可比較的 struct)與遵守最佳實踐,能讓我們的程式碼更健壯、更易維護。

掌握了 struct 的定義與初始化,接下來就可以自然地進入 介面(interface) 的設計,讓程式具備更高的抽象與彈性。祝你在 Golang 的旅程中寫出乾淨、易懂且高效的程式碼!