[奔跑的 Go] 教程十三、深入学习 Go 语言的结构(Struct)
跟传统的面向对象编程不同, go 没有类对象这样的数据结构。但是 go 语言有可以容纳复杂数据的结构体。
☛ 什么是结构体?
结构体
或结构可以与面向对象编程中的 类
概念进行比较。 如果您不知道 OOP 是什么,那么你可以将结构视为一个配方,它声明了成分和它们的量。
结构具有相同或不同类型的不同字段。 结构体主要用于需要定义由不同的单个字段( properties )组成的模式类型时。 例如** ross **
是一种**员工**
(结构),它有** firstName **
,* * LastName **
,**工资**
和** fullTime **
状态(架构字段)。
☛ 声明结构体类型
结构体 是一种包含数据结构的设计模式。 为了更方便, 我们可以使用 类型别名 来简单的声明一个结构体。可以这样生成一个结构体
type StructName struct{
field1 fieldType1
field2 fieldType2
}
在上面的例子中, StructName
是一个结构体, field1
和 field2
分别是 fieldType1
和 fieldType2
类型的字段。
我们来创建一个上面我们提到的名为Employee(雇员 )
的结构体,它包含一些字段。
type Employee struct{
firstName string
lastName string
salary int
fullTime bool
}
你可以将我们在variables
课程中学到的,将相同类型不同字段的定义写在同一行。所以我们可以编写如下程序:
type Employee struct {
firstName, lastName string
salary int
fullTime bool
}
☛ 创建一个结构体
由于我们创建了一个结构类型 Employee
,让我们用它来创建一个 ross
结构体。因为 Employee
是一种类型,所以创建该类型的变量遵循常规方法。
https://play.golang.org/p/c_Gf7YCXBJW
上面程序的输出看起来很奇怪,但是它给出了一个结构的 零值 ,因为我们只定义了结构类型 Employee
的变量 ross
,但是并没有初始化它。结构的零值是其中各自具有零值的字段。因为 string
的零值将是 ""
(空字符串被打印了出来但是您无法看到),int
的空值是 0
,bool
的空值是 false
。
当我们讨论结构体时,我们指的是包含
Employee
类型的变量。所以Employee
是 结构体类型 而ross
是 结构体,结构体是一种内置数据类型。在面向对象编程(OOP)中,我们将Employee
称之为一个类,ross
称之为一个对象,而class
是一个关键字。
☛ 获取与设置结构体的字段
获取与设置结构体的字段非常简单。当结构体被定义后,使用.
( 点 )符号,用struct.field
的语法来访问结构体字段。
在上面的程序中我们定义了一个拥有4个字段名为ross
的结构体。让我们来给ross
填充一些信息。例如,要给ross
的firstName
字段赋值,你需要使用ross.firstName = "ross"
。
https://play.golang.org/p/Fw_Z6uBCinZ
☛ 初始化结构体
我们可以在创建结构体的同时将字段赋值,而不是创建结构体后再分别给字段赋值。
https://play.golang.org/p/FGJ0Ja4WM-F
我们使用简写符号:=
来创建变量,go会自动推导出结构体类型Employee
。我们在salary
字段之前初始化了fullTime
字段,显然,字段初始化的顺序是可以随意的。
你可以只初始化一部分字段,而让其它的字段保持零值。在下面的例子中,因为薪水的零值是0
,所以结构体变量ross
语义上为 {ross Bing 0 true}
。
ross := Employee{
firstName: "ross",
lastName: "Bing",
fullTime: true,
}
还有另一种方法来初始化结构体,它没有指明字段名,如下所示。
ross := Employee{"Ross", "Geller", 1200, true}
上面的语法完全有效。在创建结构体时你没有指明字段名的话,你的字段值的顺序需要与结构体声明时定义的字段顺序保持一致。
☛ 匿名结构体
anonymous struct
就是一个没有显式定义结构体类型别名的结构体。到目前为止,我们创建了Employee
结构体类型的别名,可从ross
推导。但是在匿名结构体的情况下,我们没有定义任何结构体类型别名,使用相同的语法在行内完成结构体的创建与值的初始化。
https://play.golang.org/p/Np7Y8LuOdql
在上面的程序中,我们在创建名为monica
的结构体时没有定义结构休类型别名。
当不想重用结构体类型时,这非常有用。
你可能会有疑惑ross
的类型是Employee
,那 monica
的类型是什么呢。使用fmt.Printf
和%T
语法格式,我们得到以下结果:
struct { firstName string; lastName string; salary int; fullTime bool }
这并没有什么奇怪的。如果我们没有给它定义类型别名,它本身就是这个样子。因此我们知道要避免重复编写上面的结构体类型,我们需要定义别名。
☛ 结构体指针
我们可以在一条语句中创建指向结构体的指针,而不是创建结构。这省去了创建结构体(变量),然后再将指针指向该变量的步骤。
创建结构体指针的语法如下:
s := &StructType{...}
让我们来创建一个名为ross
的结构体指针
https://play.golang.org/p/fph03X-T-bu
在上面的程序中,由于ross
是一个指针,我们需要使用*ross
来取值,需要使用(*ross).firstName
来访问 firstName
字段,这样编译器就不会在 (*ross).firstName
和*(ross.firstName)
的语义上面搞混淆。
但是go提供了另一种语法来访问字段,例如array
指针。我们不必使用*
,而是直接使用指针来访问字段。
ross := &Employee{
firstName: "ross",
lastName: "Bing",
salary: 1200,
fullTime: true,
}
fmt.Println("firstName", ross.firstName)
☛ 匿名字段
你可以定义一个没有任何字段名的结构体类型。如果你只是定义了字段的类型,go将使用类型名来作为字段名。
https://play.golang.org/p/oIIIvnrTWdO
在上面的程序中,我们定义一个名为Data
的结构体类型,它有3个字段,名称和类型分别是string
, int
与 bool
。与其他结构体类型相比,Data
结构体类型并没有什么不同。在这个例子中,go仅仅帮我们自动创建了字段名称。你可以在字段的命名中混合一些匿名字段,如下所示。
type Employee struct {
firstName, lastName string
salary int
bool
}
☛ 嵌套结构体
结构类型的字段也可以是一个结构类型。嵌套结构体是指它的一个字段是另一个结构体。让我们看一个例子:
https://play.golang.org/p/uGsHK2ztQ5o
如上例所示, 我们创建了一个新的结构类型Salary
用来定义员工的工资。然后修改了结构类型 Employee
,将 salary
字段的类型从int
替换为 Salary
。当创建类型为Employee
的ross
结构体时,我们初始化了包括 salary
在内的所有字段。其中,初始化 salary
结构体时采用的简写方式,省略了字段名。
通常,可以使用struct.field
访问结构体的一个字段。同样,可以使用 ross.salary
访问 salary
结构体。然后,可以使用类似于ross.salary.<field>
的方式访问 (或更新) salary
的子字段。让我们来看一段程序:
https://play.golang.org/p/jVs385qTwZo
☛ 提升字段(Promoted fields)
在匿名字段章节,字段类型可以直接作为字段名。这种定义字段的方式同样适用于嵌套结构体。
https://play.golang.org/p/wzFIIOZkoqT
在上例中,删除了字段名 salary
,直接使用结构体类型 Salary
来创建匿名字段。然后可以使用 .
来 获取
和 设置
内嵌结构体 salary 中的字段。
go很酷的一点是,当我们使用匿名嵌套结构体时,父结构体可以直接访问内嵌结构体中的字段。(译者注:在一个嵌套结构体中,内嵌的匿名结构体的字段称为提升字段。) 请看下面的例子:
https://play.golang.org/p/XKn6-rKvPg9
如果内嵌的匿名结构体和父结构体有相同字段(字段名),则内层字段不会被提升,仅
非冲突
字段会被提升.
☛ 导出字段(Exported fields)
在包(packages)的课程中,任何以大写字母开头的变量或类型,都是可以从包中导出的。在本课程的学习中,为确保结构体都是可导出的,所有的 struct 命名皆以大写字母开头,如: Employee
, Salary
, Data
等等。关于结构体更酷的是,我们可以控制哪些字段允许被导出。实现方式同样遵循首字母大写
法。
type Employee struct {
FirstName, LastName string
salary int
fullTime bool
}
在上述结构体类型 Employee
中,仅 FirstName
和 LastName
两个字段是导出的。
接下来,我们以包名 org
创建一个简单的包组织。创建文件 WORKSPACE/src/org/employee.go
,写入以下代码:
// employee.go
package org
type Employee struct {
FirstName, LastName string
salary int
fullTime bool
}
在 main 包中,导入结构体 Employee
。如下所示:
// main.go
package main
import (
"fmt"
"org"
)
func main() {
ross := org.Employee{
FirstName: "Ross",
LastName: "Geller",
salary: 1200,
}
fmt.Println(ross)
}
上面的程序不会编译,编译器会抛出错误:
unknown field 'salary' in struct literal of type org.Employee
发生编译错误的原因是 salary 字段未导出。我们使用结构体类型 org.Employee
表示org
包的 Employee
。还可以在 main 包中创建 type 别名,以使其更简单。
// main.go
package main
import (
"fmt"
"org"
)
type Employee org.Employee
func main() {
ross := Employee{
FirstName: "Ross",
LastName: "Geller",
}
fmt.Println(ross)
}
程序执行结果如下:
{Ross Geller 0 false}
看起来很奇怪?竟然打印出了私有字段 salary
和 fullTime
的值。当我们从其它包导入任何结构体时,可以获取到私有字段的值,只是没有修改权限。该特性在 某些字段需要被外部使用,但不允许被篡改 时很有用。
在嵌套结构体中会发生什么呢?
- 嵌套结构体声明时必须首字母大写,以便被其它包导入。
- 嵌套结构体的字段名要首字母大写,使其可导出。
- 如果嵌套结构体是匿名的,则内层结构体中以大写字母开头的字段是提升字段(Promoted fields)。
☛ 函数式字段(Function fields)
如果你还记得函数(functions)
章节所讲的 把函数看作类型(function as type)
、 把函数当作变量的值(function as value)
,很自然会猜到 结构体(struct)
的字段也可以是函数。
接下来创建一个简单的结构体函数字段,用来 返回一个员工的全名。
https://play.golang.org/p/U73YGQcWkwa
上面的程序定义了结构体类型Employee
,拥有2个 string 类型的字段和1个 function 类型的。简单起见,创建一个 function type 别名为 FullNameType
,入参为2个 string,返回值为 string 。在 main 函数中初始化结构体e
时,要确保 FullName
遵循函数类型的语法。在上例中,为其分配了一个匿名函数。
然后传入参数e.FirstName
和 e.LastName
,执行函数e.FullName
。
如果想知道,为什么在同一个结构体
e
中,需要通过e
的属性 (即:e.FirstName
和e.LastName
)给e.FullName
也就是字段FullName
传值,你需要去看关于方法(methods)
的课程(即将更新)。
☛ 结构体的比较(struct comparison)
如果两个结构体属于同一结构体类型,并且字段值相同,则它们是可比较的。
https://play.golang.org/p/AFkN-AxDSk5
上面的程序打印结果为true
,因为 ross
和 rossCopy
都属于相同的结构体类型 Employee
,并且设置了相同的值。
然而,如果结构体具有不可比较的字段类型,如 map
,则结构体不可比较。例如下面的程序,结构体类型 Employee
含有类型为 map
的子字段,就不能进行上述比较。
type Employee struct {
firstName, lastName string
salary int
leaves map[string]int
}
☛ 结构体字段的元数据(struct field meta-data)
结构体字段可以声明元数据(meta-data)。它是一个结构体字段编码或解码时的格式(或从数据库存储 / 检索)转换信息。但你也可以用它存储任何想要的元信息,供其它包或自己使用。
这个元信息由字符串字面量(阅读 *字串数据类型(strings)*
章节的课程)定义,如下所示:
type Employee struct {
firstName string `json:"firstName"`
lastName string `json:"lastName"`
salary int `json: "salary"`
fullTime int `json: "fullTime"`
}
上面的例子,对结构体类型 Employee
进行了 JSON 编码 / 解码 。在接下来的课程中,将学习更多的结构体元数据,如 json
, validation
等。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: