[奔跑的 Go] 教程十三、深入学习 Go 语言的结构(Struct)

Golang

跟传统的面向对象编程不同, 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填充一些信息。例如,要给rossfirstName字段赋值,你需要使用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个字段,名称和类型分别是stringint 与 bool。与其他结构体类型相比,Data结构体类型并没有什么不同。在这个例子中,go仅仅帮我们自动创建了字段名称。你可以在字段的命名中混合一些匿名字段,如下所示。

type Employee struct {
    firstName, lastName string
    salary              int
    bool
}

☛ 嵌套结构体

结构类型的字段也可以是一个结构类型。嵌套结构体是指它的一个字段是另一个结构体。让我们看一个例子:

https://play.golang.org/p/uGsHK2ztQ5o

如上例所示, 我们创建了一个新的结构类型Salary用来定义员工的工资。然后修改了结构类型 Employee,将 salary 字段的类型从int替换为 Salary。当创建类型为Employeeross 结构体时,我们初始化了包括 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 命名皆以大写字母开头,如: EmployeeSalaryData 等等。关于结构体更酷的是,我们可以控制哪些字段允许被导出。实现方式同样遵循首字母大写法。

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 等。

原文地址:https://medium.com/rungo/structures-in-g...

译文地址:https://learnku.com/golang/t/29489

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 1

都到12 了

5个月前 评论

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!