相信大家都听过这样一句话:“万物皆对象”。这句话来源于面向对象编程语言,所倡导的精神是将现实的事物“抽象”出来。必要时通过各种“组成手段”将这些抽象出的“对象”重新组合在一起,最终的效果则是将现实中的万事万物描述清楚。

这种“抽象”思维的优势很明显,它可以将复杂的问题简单化,更符合人类思维,可以实现从执行者到指挥者的角色转换

Go语言脱胎于C语言(C语言是面向过程的),同时吸收了面向对象语言的编程思想和优势,极大地扩充了C语言中“结构体”的能力。作为开发者,不仅可以像传统C语言那样定义和使用结构体,还可以实现面向对象编程语言所具有的构造函数和方法、类型继承等。可以说:Go语言中的结构体使用简单、灵活,扩展性强

说得这么热闹,相信大家已经跃跃欲试,想要和“结构体”交朋友了。从本讲开始,我将使用3讲的篇幅来为大家介绍Go语言中的结构体。作为第一讲,我们要从最基本的知识点开始,具体包括:

  • 结构体的定义和使用;
  • 匿名结构体的定义和使用。

当然,如果你对前面所述的一些词汇比较陌生,也没有关系,学完这3讲,相信您不仅熟悉、会用,甚至还能给别人讲清楚呢!好了,废话不多讲,我们进入正题。

# 为何需要结构体

为什么程序中需要结构体呢?在之前的各种示例中,我们使用的通常是基本数据类型。比如int、float、bool、string。但在实际项目中,所面对的情况往往会复杂很多。

比如,要做一个图书管理系统,每本图书都有相应的属性信息:图书名称、作者名称、出版社、分类、ISBN码等等。又或者要做一个学生管理系统,每个学生也有相应的属性信息:姓名、性别、年龄、年级、班级、学号等等。像这类可以用若干属性(这些属性还有可能是不同的数据类型)来描述的“对象”,就需要使用结构体了。

所以,我们便可得出结构体的定义——结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。

💡 提示:此处所指的“类型”,可以是基本数据类型,也可以是其它结构体。本讲仅涉及由基本数据类型构成的结构体。

# 结构体的定义

还记得在上一讲中我们用过的例子吗?我们创建了两个游戏角色:“狂斩天下”和“温玉琳琅”。他们都由3个属性构成:角色名、职业和性别。

💡 提示:在实际项目中使用的结构体属性大多比本例更为复杂,但无非是数量上的增加。为了讲解方便,这里仍仅使用这3个属性为例。

在Go语言中,定义结构体的标准格式为:

go
复制代码type StructName struct {
   // 属性字段
}

其中,开头的type表示要定义自定义的类型;StructName代表结构体的名称;struct表示结构体类型;由大括号包裹的部分是属性列表,由一个或若干个字段构成。字段的名称不允许重复。

举例,若要定义用于描述游戏角色的结构体,名称为Player,则可如下实现:

go
复制代码type Player struct {
   // 角色名
   name string
   // 职业
   career string
   // 性别
   gender string
}

具体来说,本例定义了名为“Player”类型的数据,该数据的类型为结构体。这个结构体中由3个属性构成,分别使用3个字段来描述。包括角色名(name)、职业(career)和性别(gender),它们都是string类型。

实际上,Player作为自定义类型,和基本数据类型相似,同样遵循Go语言官方建议的命名方法(驼峰式),作用域的规则限制也同样适用

# 结构体的使用

一旦结构体完成定义,它就已经准备就绪,随时可用了。和基本数据类型相同,使用结构体依然需要声明和初始化。

# 结构体的声明与初始化

下面的代码声明了Player类型的变量,并完成初始化。最终创建了狂斩天下角色:

go
复制代码playerA := Player{
   name:   "狂斩天下",
   career: "战士",
   gender: "男",
}

如上代码所示,playerA就是狂斩天下了。

❗️ 注意:在初始化结构体时,每个字段需要使用逗号分割开。在定义结构体时则不用。

在初始化结构体时,可以一次性地赋值所有字段,也可以只赋值部分字段,甚至不赋值。下面几种写法都是允许的:

go
复制代码playerA := Player{
   name:   "狂斩天下",
   gender: "男",
}
go
复制代码playerA := Player{}

对于本例这种一口气赋值所有字段的行为,Go语言提供了更简洁的方式,如下所示:

go
复制代码playerA := Player{
   "狂斩天下",
   "战士",
   "男",
}

注意到区别了吗?这种简洁方式省略了所有字段名。但为了确保字段的正确匹配,需要按照结构体内部定义的顺序进行赋值,且必须一次完成所有属性赋值。即定义时是角色名、职业、性别;初始化时也应是角色名、职业、性别,顺序不能颠倒,且值缺一不可。

当我们完成初始化后,试试使用fmt.Println()函数输出playerA的值,将得到如下结果:

{狂斩天下 战士 男}

下面,请各位动手练习,声明playerB变量,并使用温玉琳琅(温玉琳琅,法师,女)的角色信息初始化它。

下面是参考代码:

go
复制代码playerB := Player{
   name:   "温玉琳琅",
   career: "法师",
   gender: "女",
}

# 访问和修改结构体的属性值

日复一日,随着游戏角色等级的不断提高,狂斩天下迎来了转职机会。他将由战士转为战神,需要改变结构体内的职业属性值。

在Go语言中,改变或完善(针对初始化时未赋值的情况)结构体内属性值的方法是非常简单的,其格式为:

go
复制代码变量名.属性名=

举例来说,狂斩天下对应的是playerA变量,职业对应的是career字段。因此,若要完成转职,应如下实现:

go
复制代码playerA.career = "战神"

再次向控制台输出playerA的值,可以看到如下输出:

{狂斩天下 战神 男}

可以看到,只有职业发生了变化,其它属性仍保持不变。

另一方面,如果我们想单独获取playerA变量的某个属性值,也可以使用类似的方式。举例来说,获取playerA的角色名,实现方式为:

go
复制代码fmt.Println(playerA.name)

运行结果为:

狂斩天下

到此,我们已经介绍了普通结构体的定义以及结构体变量的声明、初始化和字段访问(包括取值和修改)。下面我来提个问题,大家一起思考。

在上一讲中,我们使用闭包实现了工厂模式。它也能定义游戏角色,本讲的结构体也能定义游戏角色。这二者之间有何区别,为何要有不同的定义方式呢?工厂模式产出的产品——游戏角色除了包含给定的值外,还有代表血量和魔法值的HP和MP。但目前为止,结构体只能做到“给什么,有什么”。这到底是怎么一回事呢?

当我们回顾上一讲内容时,我们不难发现:闭包的目的在于起到一定的数据保护作用(对于HP、MP而言)。但是,使用结构体可以实现面向对象语言中的构造函数、方法、继承等等特性,这些却是闭包无法做到的。

# 匿名结构体

在实际开发中,还有一类情况,就是某个结构体的作用域很小,甚至只存在于某个函数内部,或是无需创建太多的该结构体变量等等。对于上述情况,Go语言允许我们使用匿名结构体简化编码,即使用匿名结构体。

💡 提示:这是本讲第二次介绍简化编码的方式了,这便是Go语言中结构体使用简便特性的体现。大家还记得上一个简化编码是用来做什么吗?答案是——初始化结构体变量

举例来说,还是游戏中的场景。某天,温玉琳琅来到许愿树下进行许愿,这一天是她的生日,许愿树这个植物仅在生日场景中出现。因此为了简化编码,考虑使用匿名结构体来定义和使用它。

使用匿名结构体的方法并不难,实际上就是省略了单独的结构体定义。而是将定义和相关变量的声明、属性赋值合三为一处理。如果我们要声明一个变量来表示许愿树,示例代码如下所示:

go
复制代码wishingTree := struct {
   height   float64
   width    float64
   treeType string
}{
   height:   22.5 * 100,
   width:    50,
   treeType: "banyan"}
fmt.Println(wishingTree)

仔细阅读上述代码,wishingTree便是表示许愿树变量了,它是结构体(struct)类型。结构体内包含3个属性,分别是浮点型的高度(height)、浮点型的胸径(width)以及字符串型的树品种。这3个属性由一个大括号包裹起来。紧随其后的大括号是为属性赋值的过程,其规则依然是允许全部赋值,也允许部分赋值。简化的赋值方式同样适用,这里不再赘述。

❗️ 注意:即使不为任何属性赋值,第二个大括号也是必不可少的,否则将引发编译时错误,程序无法被编译和运行。

运行上述代码,可见控制台如下输出:

{2250 50 banyan}

# 总结

🎉 恭喜,您完成了本次课程的学习!

📌 以下是本次课程的重点内容总结:

  • 结构体的定义和使用;
  • 匿名结构体的定义和使用。

本讲是结构体系列专题的开篇,介绍了普通结构体的定义、结构体变量的声明、赋值以及结构体内部属性的取值和修改。在赋值环节,还介绍了简化的赋值方式。

在使用结构体变量时,我们要明确的使用原则是:结构体变量和基本数据类型一样,都是变量。因此也需要声明和初始化,也遵循驼峰法的命名规则与函数作用域限制。

另外,本讲还介绍了匿名结构体,它是普通结构体的简化使用,也是实现JSON格式序列化和反序列化的基础。

关于匿名结构体的更多使用场景,包括序列化和反序列化JSON格式,感兴趣的朋友可以阅读:一文读懂Go匿名结构体使用场景 - 掘金 (juejin.cn) (opens new window)

➡️ 在下次课程中,我们会继续介绍Go语言中的结构体,具体内容是:

  • 使用结构体实现构造函数
  • 使用结构体实现方法