10 分鐘搞定 Golang 結構體

俞凡發表於2024-11-30
本文詳細介紹了 Golang 結構體(Struct)的 7 種高階技巧,包括嵌入、標籤、未匯出欄位、方法定義、結構文字、空結構體和記憶體對齊,以幫助開發者編寫更高效和可維護的 Go 程式碼。原文: Mastering Go Structs: 7 Advanced Techniques for Efficient Code

Go 結構體的功能遠不止對相關資料進行分組,如果想讓自己的 Go 程式設計技能更上一層樓,瞭解高階結構體技術至關重要。

本文將探討 7 種使用結構體的強大方法,掌握這些技巧將有助於編寫更高效、更可維護的 Go 程式碼。

Go 中的結構體是一種複合資料型別,它將變數集中在一起,是許多 Go 程式的支柱,是建立複雜資料結構和實現物件導向設計模式的基礎。但結構體的功能遠不止簡單的資料分組。

透過掌握高階結構體技術,能夠編寫出不僅更高效,而且更易於閱讀和維護的程式碼。對於任何希望建立健壯、可擴充套件應用的 Go 開發人員來說,這些技術必不可少。

讓我們深入探討這些強大的技巧!

1) 嵌入組合(Embedding for Composition)

嵌入(Embedding) 是 Go 的一項強大功能,允許我們將一個結構包含在另一個結構中,提供了一種組合機制。

與面嚮物件語言中的繼承不同,Go 語言中的嵌入是基於組合和委託的。

下面舉例說明嵌入:

package main

import "fmt"

type Address struct {
 Street  string
 City    string
 Country string
}

type Person struct {
 Name    string
 Age     int
 Address // 嵌入結構
}

func main() {
 p := Person{
  Name: "Writer",
  Age:  25,
  Address: Address{
   Street:  "abc ground 2nd floor",
   City:    "delhi",
   Country: "India",
  },
 }

 fmt.Println(p.Name)   // 輸出: Writer
 fmt.Println(p.Street) // 輸出: abc ground 2nd floor
}

本例將 Address 結構嵌入到 Person 結構中。

這樣就可以直接透過 Person 例項訪問 Address 欄位,就像訪問 Person 本身的欄位一樣。

嵌入的好處包括:

  • 程式碼複用:可以用較簡單的結構組成複雜的結構。
  • 委託:內嵌結構體的方法在外部結構體上自動可用。
  • 靈活性:如有需要,可以在外層結構中覆蓋嵌入的方法或欄位。

如果想擴充套件功能而又不希望傳統繼承那樣複雜時,嵌入就顯得尤為有用,這是 Go 用組合替代繼承方法的基石。

2) 後設資料和反射標籤(Tags for Metadata and Reflection)

Go 中的結構體標籤可以附加到結構體欄位的字串字面量上,從而提供欄位的後設資料,支援透過反射訪問。標籤廣泛用於 JSON 序列化、表單驗證和資料庫對映等任務。

下面是一個 JSON 序列化標籤的示例:

type User struct {
 ID       int    `json:"id"`
 Username string `json:"username"`
 Email    string `json:"email,omitempty"`
 Password string `json:"-"` // 在 JSON 輸出中將會被忽略
}

func main() {
 user := User{
  ID:       1,
  Username: "gopher",
  Email:    "",
  Password: "secret",
 }

 jsonData, err := json.Marshal(user)
 if err != nil {
  fmt.Println("Error:", err)
  return
 }

 fmt.Println(string(jsonData))
 // 輸出: {"id":1,"username":"gopher"}
}

在這個例子中:

  • json:"id" 標籤告訴 JSON 編碼器,在將資料轉為 JSON 時,使用 "id" 作為鍵。
  • json:"email,omitempty" 表示如果欄位為空,則省略該欄位。
  • json:"-" 表示在 JSON 輸出中不包括 Password 欄位。

要以可程式設計方式訪問標籤,可以使用 reflect 軟體包:

 t := reflect.TypeOf(User{})
 field, _ := t.FieldByName("Email")
 fmt.Println(field.Tag.Get("json"))

標籤是為結構體新增後設資料的強大方法,可使框架和庫更有效的處理資料。

3) 用於封裝的未匯出欄位(Unexported Fields for Encapsulation)

在 Go 中,封裝是透過使用匯出(大寫)和未匯出(小寫)識別符號來實現的。

當應用到結構體欄位時,這種機制允許我們控制對型別內部狀態的訪問。

下面是一個未匯出欄位的示例:

package user

type User struct {
    Username string  // 匯出欄位
    email    string  // 未匯出欄位
    age      int     // 未匯出欄位
}

func NewUser(username, email string, age int) *User {
    return &User{
        Username: username,
        email:    email,
        age:      age,
    }
}

func (u *User) Email() string {
    return u.email
}

func (u *User) SetEmail(email string) {
    // 設定前驗證郵箱
    if isValidEmail(email) {
        u.email = email
    }
}

func (u *User) Age() int {
    return u.age
}

func (u *User) SetAge(age int) {
    if age > 0 && age < 150 {
        u.age = age
    }
}

func isValidEmail(email string) bool {
    // 郵箱驗證邏輯
    return true  // 簡化示例
}

在這個例子中:

  • Username 已匯出,可從軟體包外部直接訪問。
  • emailage 未匯出,因此無法從其他軟體包直接訪問。
  • 提供了獲取方法(Email()Age()),允許讀取未匯出欄位。
  • 設定方法(SetEmail()SetAge())允許對未匯出欄位進行受控修改,包括驗證。

這種方法有幾個好處:

  • 控制資料修改:在設定數值時,可以執行驗證規則。
  • 靈活更改內部實現:可在不影響外部程式碼的情況下更改內部表示法。
  • 清晰的 API:結構支援哪些操作一目瞭然。

透過使用未匯出欄位並提供訪問和修改方法,可以建立更健壯、更易於維護的程式碼,並遵守封裝原則。

4) 結構上的方法(Methods on Structs)

在 Go 中,可以在結構型別上定義方法。這個功能非常強大,可以行為與資料關聯起來,類似於物件導向程式設計,但採用的是 Go 的獨特方法。

下面是一個使用結構體方法進行簡單快取的示例:

type CacheItem struct {
 value      interface{}
 expiration time.Time
}

type Cache struct {
 items map[string]CacheItem
 mu    sync.RWMutex
}

func NewCache() *Cache {
 return &Cache{
  items: make(map[string]CacheItem),
 }
}

func (c *Cache) Set(key string, value interface{}, duration time.Duration) {
 c.mu.Lock()
 defer c.mu.Unlock()
 c.items[key] = CacheItem{
  value:      value,
  expiration: time.Now().Add(duration),
 }
}

func (c *Cache) Get(key string) (interface{}, bool) {
 c.mu.RLock()
 defer c.mu.RUnlock()
 item, found := c.items[key]
 if !found {
  return nil, false
 }
 if time.Now().After(item.expiration) {
  return nil, false
 }
 return item.value, true
}

func (c *Cache) Delete(key string) {
 c.mu.Lock()
 defer c.mu.Unlock()
 delete(c.items, key)
}

func (c *Cache) Clean() {
 c.mu.Lock()
 defer c.mu.Unlock()
 for key, item := range c.items {
  if time.Now().After(item.expiration) {
   delete(c.items, key)
  }
 }
}

func main() {
 cache := NewCache()
 cache.Set("user1", "UnKnown", 5*time.Second)

 if value, found := cache.Get("user1"); found {
  fmt.Println("User found:", value)
 }

 time.Sleep(6 * time.Second)

 if _, found := cache.Get("user1"); !found {
  fmt.Println("User expired")
 }
}

本例定義了 Cache 結構的幾個方法:

  • Set:新增或更新快取中帶有過期時間的資料項。
  • Get:從快取中讀取資料項,檢查是否過期。
  • Delete:從快取中刪除資料項。
  • Clean:刪除快取中所有過期的資料項。

請注意,在修改快取的方法中使用了指標接收器(*Cache),而在只從快取讀取資料的方法中使用了值接收器。這種模式在 Go 中很常見:

  • 當方法需要修改輸入資料或結構體較大以避免複製時,可使用指標接收器。
  • 當方法不修改輸入資料且結構很小時,使用值接收器。

透過結構體上的方法,可以為型別建立簡潔、直觀的 API,使程式碼更有條理、更易於使用。

5) 結構體字面量和命名欄位(Struct Literals and Named Fields)

Go 提供了靈活的語法來初始化結構體,即結構體字面量(struct literals)。在結構體字面量中使用命名欄位可以大大提高程式碼可讀性和可維護性,對於欄位較多的結構體尤其有用。

我們以大型結構體為例,看看如何使用命名欄位對其進行初始化:

type Server struct {
 Host            string
 Port            int
 Protocol        string
 Timeout         time.Duration
 MaxConnections  int
 TLS             bool
 CertFile        string
 KeyFile         string
 AllowedIPRanges []string
 DatabaseURL     string
 CacheSize       int
 DebugMode       bool
 LogLevel        string
}

func main() {
 // 沒用命名欄位(難以閱讀且容易出錯)
 server1 := Server{
  "localhost",
  8080,
  "http",
  30 * time.Second,
  1000,
  false,
  "",
  "",
  []string{},
  "postgres://user:pass@localhost/dbname",
  1024,
  true,
  "info",
 }

 // 使用命名欄位(可讀性和可維護性更強)
 server2 := Server{
  Host:            "localhost",
  Port:            8080,
  Protocol:        "http",
  Timeout:         30 * time.Second,
  MaxConnections:  1000,
  TLS:             false,
  AllowedIPRanges: []string{},
  DatabaseURL:     "postgres://user:pass@localhost/dbname",
  CacheSize:       1024,
  DebugMode:       true,
  LogLevel:        "info",
 }

 fmt.Printf("%+v\n", server1)
 fmt.Printf("%+v\n", server2)
}

在結構體字面量中使用命名欄位有幾個優點:

  • 可讀性:每個值對應的內容一目瞭然。
  • 可維護性:可以輕鬆新增、刪除或重新排列欄位,而無需修改現有程式碼。
  • 部分初始化:可以只初始化需要的欄位,其餘欄位的值為零。
  • 自文件化:程式碼本身記錄了每個值的用途。

在重構大型結構體或處理複雜配置時,使用命名欄位可以大大提高程式碼清晰度,降低出錯的可能性。

6) 訊號空結構體(Empty Structs for Signaling)

Go 中的空結構體是指沒有欄位的結構體。

宣告為 struct{},佔用的儲存空間為零位元組。

這種獨特屬性使得空結構體在某些情況下非常有用,尤其是在併發程式中發出訊號或實現集合時。

下面是基於空結構體實現執行緒安全集合的示例:

type Set struct {
 items map[string]struct{}
 mu    sync.RWMutex
}

func NewSet() *Set {
 return &Set{
  items: make(map[string]struct{}),
 }
}

func (s *Set) Add(item string) {
 s.mu.Lock()
 defer s.mu.Unlock()
 s.items[item] = struct{}{}
}

func (s *Set) Remove(item string) {
 s.mu.Lock()
 defer s.mu.Unlock()
 delete(s.items, item)
}

func (s *Set) Contains(item string) bool {
 s.mu.RLock()
 defer s.mu.RUnlock()
 _, exists := s.items[item]
 return exists
}

func (s *Set) Len() int {
 s.mu.RLock()
 defer s.mu.RUnlock()
 return len(s.items)
}

func main() {
 set := NewSet()
 set.Add("apple")
 set.Add("banana")
 set.Add("apple") // 重複資料,不會被新增

 fmt.Println("Set contains 'apple':", set.Contains("apple"))
 fmt.Println("Set size:", set.Len())

 set.Remove("apple")
 fmt.Println("Set contains 'apple' after removal:", set.Contains("apple"))
}

本例中使用 map[string]struct{} 實現集合。在 map 中使用空結構體 struct{}{} 作為值,因為:

  • 不佔用任何記憶體空間。
  • 我們只關心鍵是否存在,不關心任何相關的值。

空結構體還可用於併發程式中的訊號傳遞。例如:

done := make(chan struct{})

go func() {
    // 乾點啥
    // ...
    close(done)  // 幹完後傳送訊號
}()

<-done  // 等待 goroutine 結束

在這種情況下,我們對通道傳遞的任何資料都不感興趣,只想發出工作完成的訊號。

空結構體不會分配任何記憶體,非常適合這種場景。

在某些情況下,透過這種方式使用空結構體可以使程式碼更高效、更清晰。

7) 結構對齊和填充(Struct Alignment and Padding)

瞭解結構對齊和填充對於最佳化 Go 程式的記憶體使用至關重要,尤其是在處理大量結構例項或進行系統程式設計時。

與許多程式語言一樣,Go 會對記憶體中的結構體欄位進行對齊,以提高訪問效率。

這種對齊方式會在欄位之間引入填充,從而增加結構體的整體大小。

下面舉例說明這一概念:

type Inefficient struct {
 a bool  // 1 byte
 b int64 // 8 bytes
 c bool  // 1 byte
}

type Efficient struct {
 b int64 // 8 bytes
 a bool  // 1 byte
 c bool  // 1 byte
}

func main() {
 inefficient := Inefficient{}
 efficient := Efficient{}

 fmt.Printf("Inefficient: %d bytes\n", unsafe.Sizeof(inefficient))
 fmt.Printf("Efficient: %d bytes\n", unsafe.Sizeof(efficient))
}

執行這段程式碼,會輸出:

Inefficient: 24 bytes
Efficient: 16 bytes

儘管包含相同欄位,但 Inefficient 結構體佔用 24 個位元組,而 Efficient 結構體只佔用 16 個位元組。這種差異是由於填充造成的:

  • Inefficient 結構體中:

    • a 佔用 1 個位元組,然後是 7 個位元組的填充,用於對齊 b
    • b 佔用 8 個位元組。
    • c 佔用 1 個位元組,然後是 7 個位元組的填充,以保持對齊。
  • Efficient 結構體中:

    • b 佔用 8 個位元組。
    • ac 各佔 1 個位元組,末尾有 6 個位元組的填充。

最佳化結構記憶體的使用:

  • 將欄位從大到小排序。
  • 將大小相同的欄位分組。

瞭解並最佳化結構佈局可以大大節省記憶體,尤其是在處理大量結構例項或在記憶體受限的系統中工作時。

結論

這些技術是編寫符合慣例、高效、可維護的 Go 程式碼的基本工具。利用這些方法,可以建立更具表現力的資料結構,改進程式碼組織,最佳化記憶體使用,並充分利用 Go 強大的型別系統。

掌握了這些高階結構體技術,就能更好的應對複雜的程式設計挑戰,編寫出效能卓越且易於理解的 Go 程式碼。

請記住,熟練掌握這些技術的關鍵在於實踐。將它們融入到專案中,嘗試不同方法,並始終考慮複雜性、效能和可維護性之間的權衡。


你好,我是俞凡,在Motorola做過研發,現在在Mavenir做技術工作,對通訊、網路、後端架構、雲原生、DevOps、CICD、區塊鏈、AI等技術始終保持著濃厚的興趣,平時喜歡閱讀、思考,相信持續學習、終身成長,歡迎一起交流學習。為了方便大家以後能第一時間看到文章,請朋友們關注公眾號"DeepNoMind",並設個星標吧,如果能一鍵三連(轉發、點贊、在看),則能給我帶來更多的支援和動力,激勵我持續寫下去,和大家共同成長進步!

本文由mdnice多平臺釋出

相關文章