10 分钟搞定 Golang 结构体

本文详细介绍了 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多平台发布

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容