0%

Go语言学习笔记5-结构体

[TOC]

Go 语言通过用自定义的方式形成新的类型,结构体是类型中带有成员的复合类型
Go 语言使用结构体和结构体成员来描述真实世界的实体和实体对应的各种属性

Go 语言中的类型可以被实例化,使用new&构造的类型实例的类型是类型的指针

结构体成员是由一系列的成员变量构成,这些成员变量也被称为“字段”。字段有以下特性:

  • 字段拥有自己的类型和值
  • 字段名必须唯一
  • 字段的类型也可以是结构体,甚至是字段所在结构体的类型

关于 Go 语言的类(class)

Go 语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。

Go 语言的结构体与“类”都是复合结构体,但 Go 语言中结构体的内嵌配合接口比面向对象具有更高的扩展性和灵活性。

Go 语言不仅认为结构体能拥有方法,且每种自定义类型也可以拥有自己的方法

Go 语言结构体定义

Go 语言的关键字type 可以将各种基本类型定义为自定义类型,基本类型包括整型、字符串、布尔等。结构体是一种复合的基本类型,通过 type 定义为自定义类型后,使用结构体更便于使用

结构体的定义格式如下:

1
2
3
4
5
6
7
8
9
10
11
type 类型名 struct {
字段1 字段1类型
字段2 字段2类型
。。。
}

type point struct {
x int
y int
xx, yy int
}

Go语言实例化结构体-结构体分配内存并初始化

结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存。因此必须在定义结构体并实例化后才能使用结构体的字段

实例化就是根据结构体定义的格式创建一份与格式一致的内存区域,结构体实例与实例间的内存是完全独立的

Go 语言可以通过多种方式实例化结构体,根据实际需要可以选用不同的写法。

基本的实例化形式

结构体本身是一种类型,可以像整形、字符串等类型一样,以var 的方式声明结构体即可完成实例化

基本实例化格式如下:

1
var ins T

其中,T 为结构体类型,ins 为结构体的实例

1
2
3
4
5
6
7
8
9
10
func main() {
type point struct {
x int
y int
}
var p point
p.x = 10
p.y = 20
fmt.Println(p) // {10 20}
}

指针类型的结构体

Go 语言中,还可以使用 new 关键字对类型(包括结构体、整形。浮点数。字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体

使用new 的格式如下:

1
ins := new(T)

其中:

  • T 为类型,可以是结构体、整形、字符串等
  • ins :T 类型被实例化后保存到ins 变量中,ins 的类型为 *T,属于指针

Go 语言让我们可以像访问普通结构体一样使用.访问结构体指针的成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
structDemo()
}

func structDemo() {
type Player struct {
Name string
HealthPoint int
MagicPoint int
}
factory := new(Player)
factory.Name = "鲁班"
factory.HealthPoint = 300
fmt.Println(factory) // &{鲁班 300 0}
}

经过 new 实例化的结构体实例在成员赋值上与基本实例化的写法一致。

在 Go 语言中,访问结构体指针的成员变量时可以继续使用.。这是因为 Go 语言为了方便开发者访问结构体指针的成员变量,使用了语法糖(Syntactic sugar)技术,将 ins.Name 形式转换为 (*ins).Name。

取结构体的地址实例化

在Go语言中,对结构体进行 & 取地址操作时,视为对该类型进行一次new 的实例化操作,取地址格式如下:

1
ins := &T{}

其中:

  • T 表示结构体类型
  • ins 为结构体的实例,类型为 *T ,是指针类型

下面使用结构体定义一个命令行指令(Command),指令中包含名称、变量关联和注释等。对 Command 进行指针地址的实例化,并完成赋值过程,代码如下:

1
2
3
4
5
6
7
8
9
10
type Command struct {
Name string // 指令名称
Var *int // 指令绑定的变量
Comment string // 指令的注释
}
var version int = 1
cmd := &Command{}
cmd.Name = "version"
cmd.Var = &version
cmd.Comment = "show version"

取地址实例化是最广泛的一种结构体实例化方式。可以使用函数封装上面的初始化过程,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
func newCommand(name string, varref *int, comment string) *Command {
return &Command{
Name: name,
Var: varref,
Comment: comment,
}
}
cmd = newCommand(
"version",
&version,
"show version",
)

Go语言初始化结构体的成员变量

结构体在实例化时可以直接对成员变量进行初始化。初始化有两种形式:

  1. 字段“键值对”形式
  2. 多个值的列表形式

键值对形式的初始化适合选择性填充字段较多的结构体;

多个值的列表形式适合填充字段较少的结构体

使用键值对初始化结构体

结构体可以使用键值对初始化字段,每个键对应结构体中的一个字段。键的值对应字段需要初始化的值,键值对的填充是可选的,不需要初始化的字段可以不填入初始化列表中

结构体实例化后字段的默认值是字段类型的默认值。例如:数值为 0,字符串为空字符串,布尔为 false,指针为 nil 等

键值对初始化结构体格式

如下

1
2
3
4
5
ins := 结构体类型名{
字段1: 字段1的值,
字段2:字段2的值,
...
}

下面是对各个部分的说明:

  • 结构体类型:定义结构体时的类型名称。
  • 字段1、字段2:结构体的成员字段名。结构体类型名的字段初始化列表中,字段名只能出现一次。
  • 字段1的值、字段2的值:结构体成员字段的初始值。

键值之间以:分隔;键值对之间以,分隔。

键值对初始化结构体的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type People struct {
name string
child *People
}

relation := &People{
name: "爷爷",
child: &People{
name: "爸爸",
child: &People{
name: "孩子",
},
},
}

结构体成员中只能包含结构体的指针类型,包含非指针类型会引起编译错误。

使用多个值得列表初始化结构体

Go 语言可以在键值对初始化的基础上忽略键,也就是说,可以使用多个值的列表初始化结构体的字段

多个值列表初始化结构体的书写格式

1
2
3
4
5
ins := 结构体名{
字段1的值,
字段2的值,
...
}

使用这种格式初始化时,需要注意:

  • 必须初始化结构体的所有字段
  • 每一个初始值得填充顺序必须与结构体中的声明顺序一致
  • 键值对与值列表的初始化形式不能混用

多个值列表初始化结构体的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
type Address struct {
Province string
City string
ZipCode int
PhoneNumber string
}
addr := Address{
"四川",
"成都",
610000,
"0",
}
fmt.Println(addr) // {四川 成都 610000 0}

初始化匿名结构体

匿名结构体没有类型名称,无需通过 type 关键字定义就可以直接使用

匿名结构体定义格式和初始化写法

匿名结构体的初始化写法由结构体定义键值对初始化两部分组成
结构体定义时没有结构体类型名,只有字段和字段类型的定义
键值对初始化部分由可选的多个键值对组成,如下格式:

1
2
3
4
5
6
7
8
9
10
11
ins := struct {
// 匿名结构体字段定义
字段1 字段1类型
字段2 字段2类型
...
}{
// 字段值初始化
初始化字段1: 字段1的值,
初始化字段2: 字段2的值,
...
}

键值对初始化部分是可选的,不初始化成员时,匿名结构体的格式为:

1
2
3
4
5
6
ins := struct {
// 匿名结构体字段定义
字段1 字段1类型
字段2 字段2类型
...
}

使用匿名结构体的例子

在本例中,使用匿名结构体的方式定义和初始化一个消息结构,这个消息结构具有消息标示部分(ID)和数据部分(data)。打印消息内容的 printMsg() 函数在接收匿名结构体时需要在参数上重新定义匿名结构体,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import (
"fmt"
)
// 打印消息类型, 传入匿名结构体
func printMsgType(msg *struct {
id int
data string
}) {
// 使用动词%T打印msg的类型
fmt.Printf("%T\n", msg)
}
func main() {
// 实例化一个匿名结构体
msg := &struct { // 定义部分
id int
data string
}{ // 值初始化部分
1024,
"hello",
}
printMsgType(msg) // *struct { id int; data string }
}

代码说明如下:

  • 第 6 行,定义 printMsgType() 函数,参数为 msg,类型为 *struct{id int data string}。因为类型没有使用 type 定义,所以需要在用到的地方每次进行定义。
  • 第 11 行,使用字符串格式化中的%T动词,将 msg 的类型名打印出来。
  • 第 15 行,对匿名结构体进行实例化,同时初始化成员。
  • 第 16 和 17 行,定义匿名结构体的字段。
  • 第 19 和 20 行,给匿名结构体字段赋予初始值。
  • 第 22 行,将 msg 传入 printMsgType() 函数中进行函数调用。

匿名结构体的类型名是结构体包含字段成员的详细描述。匿名结构体在使用时需要重新定义,造成大量重复的代码,因此开发中较少使用。

Go语言的构造函数

Go 语言的类型或结构体没有构造函数的功能,结构体的初始化过程可以使用函数封装实现

TODO 没搞懂

Go语言方法和接收器

Go语言中的方法是一种作用于特定类型变量的函数。这种特定类型变量叫做接收器(Receiver)

如果将特定类型理解为结构体或类时,接收器的概念就类似于其他语言中的 this 或者 self

在Go语言中,接收器的类型可以使任何类型,不仅仅是结构体,任何类型都可以拥有方法

提示

在面向对象的语言中,类拥有的方法一般被理解为类可以做的事情。在 Go 语言中“方法”的概念与其他语言一致,只是 Go 语言建立的“接收器”强调方法的作用对象是接收器,也就是类实例,而函数没有作用对象。

为结构体添加方法

面向过程实现方法

面向过程中没有方法 的概念,只能通过结构体和函数,有使用者使用函数参数和调用关系来形成接近方法的概念:

1
2
3
4
5
6
7
8
9
10
11
type Bag struct {
items []int
}
// 将一个物品放入背包的过程
func Insert(b *Bag, itemid int) {
b.items = append(b.items, itemid)
}
func main() {
bag := new(Bag)
Insert(bag, 1001)
}

Insert() 函数将 *Bag 参数放在第一位,强调 Insert 会操作 *Bag 结构体。但实际使用中,并不是每个人都会习惯将操作对象放在首位。一定程度上让代码失去一些范式和描述性。同时,Insert() 函数也与 Bag 没有任何归属概念。随着类似 Insert() 的函数越来越多,面向过程的代码描述对象方法概念会越来越麻烦和难以理解。

Go语言的结构体方法

1
2
3
4
5
6
7
8
9
10
type Bag struct {
items []int
}
func (b *Bag) Insert(itemid int) {
b.items = append(b.items, itemid)
}
func main() {
b := new(Bag)
b.Insert(1001)
}

每个方法只能有一个接收器,如下图所示。

接收器-方法作用的目标

接收器的格式如下:

1
2
3
func (接收器变量 接收器类型) 方法名(参数列表)(返回参数){
函数体
}

对各部分的说明:

  • 接收器变量:接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母,而不是 self、this 之类的命名。例如,Socket 类型的接收器变量应该命名为 s,Connector 类型的接收器变量应该命名为 c 等。
  • 接收器类型:接收器类型和参数类似,可以是指针类型和非指针类型。
  • 方法名、参数列表、返回参数:格式与函数定义一致。

接收器根据接收器的类型可以分为指针接收器、非指针接收器。两种接收器在使用时会产生不同的效果。根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中。

指针类型接收器

指针类型的接收器由一个结构体的指针组成,更接近与面向对象中的this 或者 self
由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import "fmt"
// 定义属性结构
type Property struct {
value int // 属性值
}
// 设置属性值
func (p *Property) SetValue(v int) {
// 修改p的成员变量
p.value = v
}
// 取属性值
func (p *Property) Value() int {
return p.value
}
func main() {
// 实例化属性
p := new(Property)
// 设置值
p.SetValue(100)
// 打印值
fmt.Println(p.Value()) // 100
}

非指针类型接收器

当方法作用于非指针接收器时,Go 语言会在代码运行时将接收器的值复制一份。在非指针接收器的方法中可以获取接收器的成员值,但修改后无效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import (
"fmt"
)
// 定义点结构
type Point struct {
X int
Y int
}
// 非指针接收器的加方法
func (p Point) Add(other Point) Point {
// 成员值与参数相加后返回新的结构
return Point{p.X + other.X, p.Y + other.Y}
}
func main() {
// 初始化点
p1 := Point{1, 1}
p2 := Point{2, 2}
// 与另外一个点相加
result := p1.Add(p2)
// 输出结果
fmt.Println(result) // {3 3}
}

由于例子中使用了非指针接收器,Add() 方法变得类似于只读的方法,Add() 方法内部不会对成员进行任何修改

指针和非指针接收器的使用

在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器。大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针。

Go语言为任意类型添加方法

Go语言可以对任何类型添加方法。给一种类型添加方法就像给结构体添加方法一样,因为结构体也是一种类型

为基本类型添加方法

在Go语言中,使用 type 关键字可以定义出新的自定义类型。之后就可以为自定义类型添加各种方法。我们习惯于使用面向过程的方法判断一个值是否为0,例如:

1
2
3
if v == 0 {
// v = 0
}

如果将 v 比作整型对象,那么判断 v 值就可以增加一个IsZero() 方法,通过这个方法就可以判断v 值是否为0,例如:

1
2
3
if v.IsZero(){
// v = 0
}

为基本类型添加方法的详细流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
"fmt"
)
// 将int定义为MyInt类型
type MyInt int
// 为MyInt添加IsZero()方法
func (m MyInt) IsZero() bool {
return m == 0
}
// 为MyInt添加Add()方法
func (m MyInt) Add(other int) int {
return other + int(m)
}
func main() {
var b MyInt = 2
fmt.Println(b.IsZero()) // false
b = 1
fmt.Println(b.Add(2)) //3
}

Go语言类型内嵌和结构体内嵌

结构体允许其成员字段在声明时没有字段名而只有类型,这种形式的字段被称为类型内嵌匿名字段类型内嵌,写法如下:

1
2
3
4
5
6
7
8
9
10
type Data struct {
int
float32
bool
}
ins := &Data {
int: 10
float32: 3.14,
bool: true
}

类型内嵌其实仍然拥有自己的字段名,只是字段名就是其类型本身而已,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个

结构体实例化后,如果匿名的字段类型为结构体,那么可以直接访问匿名结构体里的所有成员,这种方式被称为结构体内嵌

声明结构体内嵌

结构体类型内嵌比普通类型内嵌的概念复杂一些,下面通过一个实例来理解。

计算机图形学中的颜色有两种类型,一种是包含红、绿、蓝三原色的基础颜色;另一种是在基础颜色之外增加透明度的颜色。透明度在颜色中叫 Alpha,范围为 0~1 之间。0 表示完全透明,1 表示不透明。使用传统的结构体字段的方法定义基础颜色和带有透明度颜色的过程代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main
import (
"fmt"
)
// 基础颜色
type BasicColor struct {
// 红、绿、蓝三种颜色分量
R, G, B float32
}
// 完整颜色定义
type Color struct {
// 将基本颜色作为成员
Basic BasicColor
// 透明度
Alpha float32
}
func main() {
var c Color
// 设置基本颜色分量
c.Basic.R = 1
c.Basic.G = 1
c.Basic.B = 0
// 设置透明度
c.Alpha = 1
// 显示整个结构体内容
fmt.Printf("%+v", c) // {Basic:{R:1 G:1 B:0} Alpha:1}
}

这种写法虽然合理但是写法很复杂。使用 Go 语言的结构体内嵌写法重新调整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import (
"fmt"
)
type BasicColor struct {
R, G, B float32
}
type Color struct {
BasicColor
Alpha float32
}
func main() {
var c Color
c.R = 1
c.G = 1
c.B = 0
c.Alpha = 1
fmt.Printf("%+v", c)
}

第14-16 行,可以直接对 Color 的 R、G、B 成员进行设置,编译器通过 Color 的定义知道 R、G、B 成员来自 BasicColor 内嵌的结构体。

结构内嵌特性

Go 语言的结构体内嵌有如下特性:

1. 内嵌的结构体可以直接访问其成员变量

嵌入结构体的成员,可以通过外部结构体的实例直接访问。如果结构体有多层嵌入结构体,结构体实例访问任意一级的嵌入结构体成员时都只用给出字段名,而无须像传统结构体字段一样,通过一层层的结构体字段访问到最终的字段。例如,ins.a.b.c的访问可以简化为ins.c。

2. 内嵌结构体的字段名是它的类型名

内嵌结构体字段仍然可以使用详细的字段进行一层层访问,内嵌结构体的字段名就是它的类型名,代码如下:

1
2
3
4
var c Color
c.BasicColor.R = 1
c.BasicColor.G = 1
c.BasicColor.B = 0

一个结构体只能嵌入一个同类型的成员,无须担心结构体重名和错误赋值的情况,编译器在发现可能的赋值歧义时会报错。

Go语言结构体内嵌模拟类的继承

在面向对象思想中,实现对象关系需要使用“继承”特性。例如,人类不能飞行,鸟类可以飞行。人类和鸟类都可以继承自可行走类,但只有鸟类继承自飞行类。

Go 语言 的结构体内嵌特性就是一种组合特性,使用组合特性可以快速构建对象的不同特性。

下面的代码使用 Go 语言的结构体内嵌实现对象特性组合,请参考下面的代码。

人和鸟的特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main
import "fmt"
// 可飞行的
type Flying struct{}
func (f *Flying) Fly() {
fmt.Println("can fly")
}
// 可行走的
type Walkable struct{}
func (f *Walkable) Walk() {
fmt.Println("can calk")
}
// 人类
type Human struct {
Walkable // 人类能行走
}
// 鸟类
type Bird struct {
Walkable // 鸟类能行走
Flying // 鸟类能飞行
}
func main() {
// 实例化鸟类
b := new(Bird)
fmt.Println("Bird: ")
b.Fly()
b.Walk()
// 实例化人类
h := new(Human)
fmt.Println("Human: ")
h.Walk()
}

运行代码,输出如下:

1
2
3
4
5
Bird:
can fly
can calk
Human:
can calk

使用 Go 语言的内嵌结构体实现对象特性,可以自由地在对象中增、删、改各种特性。Go 语言会在编译时检查能否使用这些特性。

Go语言初始化内嵌结构体

结构体内嵌初始化时,将结构体内嵌的类型作为字段名像普通结构体一样进行初始化,详细实现过程请参考下面的代码。

车辆结构的组装和初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main
import "fmt"
// 车轮
type Wheel struct {
Size int
}
// 引擎
type Engine struct {
Power int // 功率
Type string // 类型
}
// 车
type Car struct {
Wheel
Engine
}
func main() {
c := Car{
// 初始化轮子
Wheel: Wheel{
Size: 18,
},
// 初始化引擎
Engine: Engine{
Type: "1.4T",
Power: 143,
},
}
fmt.Printf("%+v\n", c)
}

初始化内嵌匿名结构体

在前面描述车辆和引擎的例子中,有时考虑编写代码的便利性,会将结构体直接定义在嵌入的结构体中。也就是说,结构体的定义不会被外部引用到。在初始化这个被嵌入的结构体时,就需要再次声明结构才能赋予数据。具体请参考下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main
import "fmt"
// 车轮
type Wheel struct {
Size int
}
// 车
type Car struct {
Wheel
// 引擎
Engine struct {
Power int // 功率
Type string // 类型
}
}
func main() {
c := Car{
// 初始化轮子
Wheel: Wheel{
Size: 18,
},
// 初始化引擎
Engine: struct {
Power int
Type string
}{
Type: "1.4T",
Power: 143,
},
}
fmt.Printf("%+v\n", c)
}