7 個實用技巧幫助你更好地玩轉 Go Structs
在本文中,我們將探討使用結構體的 7 個技巧,掌握它們能夠幫助開發者寫出更高效、更可維護的 Go 代碼。
Go 中的結構體是一種複合數據類型,它將變量集中在一個名稱下。它們是許多 Go 程序的支柱,是創建複雜數據結構和實現面向對象設計模式的基礎。但結構體的功能遠不止簡單的數據分組。
1. Embedding
Embedding 是 Go 的一項強大功能,它允許將一個結構包含在另一個結構中,提供了一種合成機制。與面嚮對象語言中的繼承不同,Go 語言中的嵌入是關於組合和委託的。
下面舉例說明 Embedding:
package main
import "fmt"
type Address struct {
Street string
City string
Country string
}
type Person struct {
Name string
Age int
Address // Embedded struct
}
func main() {
p := Person{
Name: "Writer",
Age: 25,
Address: Address{
Street: "abc ground 2nd floor",
City: "delhi",
Country: "India",
},
}
fmt.Println(p.Name) // Outputs: Writer
fmt.Println(p.Street) // Outputs: abc ground 2nd floor
}
在本例中,我們將地址結構嵌入到人員結構中。
這樣,我們就可以直接通過 Person 實例訪問 Address 字段,就像訪問 Person 本身的字段一樣。
Embedding 的好處包括:
-
代碼重用:可以用較簡單的結構組成複雜的結構。
-
委託:內嵌結構體的方法在外部結構體上自動可用。
-
靈活性:如有需要,我們可以覆蓋外層結構中的嵌入式方法或字段。
當你想擴展功能而又不想像傳統繼承那樣複雜時,嵌入就顯得尤爲有用。它是 Go 繼承之上的組合方法的基石。
Tags for Metadata and Reflection
Go 中的 Struct 標記是可以附加到 struct 字段的字符串字面量。它們提供了字段的元數據,可以通過反射訪問。標籤廣泛用於 JSON 序列化、表單驗證和數據庫映射等任務。下面是一個使用 JSON 序列化標籤的示例:
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email,omitempty"`
Password string `json:"-"` // Will be omitted from JSON output
}
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))
// Output: {"id":1,"username":"gopher"}
}
在這個例子中:
-
json: "id" 標記會告訴 JSON 編碼器,在將數據轉爲 JSON 時,使用 "id" 作爲鍵。
-
json: "email,omitempty" 表示如果字段爲空,則省略該字段。
-
json:"-" 表示在 JSON 輸出中不包括密碼字段。
要以代碼的方式訪問標籤,可以使用 reflect 軟件包:
t := reflect.TypeOf(User{})
field, _ := t.FieldByName("Email")
fmt.Println(field.Tag.Get("json"))
標籤是爲結構體添加元數據的強大方法,可使框架和庫更有效地處理數據。
用於封裝的未導出字段
在 Go 中,封裝是通過使用導出(大寫)和未導出(小寫)標識符來實現的。當應用到 struct 字段時,這種機制允許控制對類型內部狀態的訪問。下面是一個未導出字段的示例:
package user
type User struct {
Username string // Exported field
email string // Unexported field
age int // Unexported field
}
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) {
// Validate email before setting
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 {
// logic for validating email address
return true // Simplified for this example
}
Username 已導出,可從軟件包外部直接訪問。
email 和 age 字段未導出,因此無法從其他軟件包直接訪問。但是我們提供了獲取方法(Email() 和 Age()),允許讀取未導出字段 設置方法(SetEmail() 和 SetAge())允許對未導出字段進行受控修改,包括驗證。
這種方法有幾個好處:
-
控制數據修改:在設置數值時,可以執行驗證規則。
-
靈活更改內部實現:可在不影響外部代碼的情況下更改內部表示法。
-
清晰的 API:結構支持哪些操作一目瞭然。
通過使用未導出字段並提供訪問和修改方法,可以創建更健壯、更易於維護的代碼,並遵守封裝原則。
Methods on Structs
在 Go 中,可以在結構類型上定義方法。這是一個強大的功能,它允許將行爲與數據關聯起來,類似於面向對象編程,但採用的是 Go 獨特的方法。
下面是一個使用 struct 方法進行簡單緩存的示例:
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")
}
}
-
Set添加或更新緩存中帶有過期時間的項目。 -
Get從緩存中讀取項目,檢查是否過期。 -
Delete從緩存中刪除項目。 -
Clean刪除緩存中所有過期項目。
需要注意的是在修改緩存的方法中使用了指針接收器 (*Cache),而在只從緩存讀取數據的方法中使用了值接收器。這是 Go 中常見的模式:
-
當方法需要修改接收器或結構體較大以避免複製時,可使用指針接收器。
-
當方法不修改接收器且結構很小時,使用值接收器。
通過結構體上的方法,可以爲類型創建簡潔、直觀的 API,使代碼更有條理、更易於使用。
結構字面和命名字段
Go 提供了一種靈活的語法來初始化結構體,即 struct literals。在 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() {
// Without named fields (hard to read and error-prone)
server1 := Server{
"localhost",
8080,
"http",
30 * time.Second,
1000,
false,
"",
"",
[]string{},
"postgres://user:pass@localhost/dbname",
1024,
true,
"info",
}
// With named fields (much more readable and maintainable)
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)
}
在結構文字中使用命名字段有幾個優點:
-
可讀性:每個值對應的內容一目瞭然。
-
可維護性:可以輕鬆添加、刪除或重新排列字段,而無需破壞現有代碼。
-
部分初始化:可以只初始化所需的字段,其餘字段的值爲零。
-
文檔化:代碼本身記錄了每個值的用途。
在重構大型結構或處理複雜配置時,使用命名字段可以大大提高代碼的清晰度,並降低出錯的可能性。
Empty Structs
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") // Duplicate, won't be added
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 struct{}{} 作爲值,因爲:
-
它不佔用任何內存空間。
-
我們只關心鍵的存在,而不關心任何相關的值。
空結構體還可用於併發程序中的信號傳遞。例如:
done := make(chan struct{})
go func() {
// Do some work
// ...
close(done) // Signal that work is complete
}()
<-done // Wait for the goroutine to finish
在這種情況下,我們對通過通道傳遞任何數據都不感興趣,我們只想發出工作完成的信號。空結構非常適合,因爲它不會分配任何內存。在某些情況下,以這些方式使用空結構體可以使代碼更高效、更清晰。
結構對齊和填充
瞭解結構對齊和填充對於優化 Go 程序中的內存使用至關重要,尤其是在處理大量結構實例或進行系統編程時。與許多編程語言一樣,Go 會對內存中的 struct 字段進行對齊,以提高訪問效率。
這種對齊方式會在字段之間引入填充,從而增加結構體的整體大小。下面舉例說明這一概念:
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
儘管包含相同的字段,低效結構體佔用 24 個字節,而高效結構體只佔用 16 個字節。這種差異是由於填充造成的:
- 在效率低下的結構中:
-
a 佔用 1 個字節,然後是 7 個字節的填充,用於對齊 b。
-
b 佔用 8 個字節。
-
c 佔用 1 個字節,然後是 7 個字節的填充,以保持對齊。
- 在高效結構中:
-
b 佔用 8 個字節。
-
a 和 c 各佔 1 個字節,末尾有 6 個字節的填充。
優化結構內存的使用:
-
將字段從大到小排序。
-
將大小相同的字段分組。
瞭解並優化結構佈局可以大大節省內存,尤其是在處理大量結構實例或在內存受限的系統中工作時。
總結
這些技術是編寫習慣化、高效和可維護的 Go 代碼的基本工具。開發者可以創建更具表現力的數據結構,改進代碼組織,優化內存使用,並充分利用 Go 強大的類型系統。開發者熟練掌握這些技術的關鍵在於實踐,嘗試將它們融入到平日的項目中,嘗試不同的方法,並始終考慮複雜性、性能和可維護性之間的權衡。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/eRpjQn9GOZCV3prQsrQLpA