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" 標籤。 |
最佳實踐小結
- 欄位名稱字面量:可讀性最高,防止未來欄位變更造成錯誤。
- 工廠函式:集中驗證與預設值,提升程式的安全性。
- 指標 vs 值:根據資料大小與是否需要修改決定傳遞方式。
- 使用標籤:與外部系統(JSON、DB、ORM)互動時,標籤是必備工具。
- 保持不可變性:若 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 的旅程中寫出乾淨、易懂且高效的程式碼!