Go语言中没有“类”的概念,也不支持“类”的继承等面向对象等概念。GO语言的结构体与“类”都是复合结构体,但是Go语言中的结构体内嵌配合接口比面向对象具有更高的扩展性和灵活性。

结构体的定义

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

1
2
3
4
5
type 结构体名称 struct {
字段1 字段1类型
字段2 字段2类型
...
}

列如表示一个包含 X 和 Y 整型分量的点结构,代码如下:

1
2
3
4
type Point struct {
X int
Y int
}

相同类型的变量也可以写在一行,之间使用逗号隔开,比如一个定义颜色的结构体:

1
2
3
type Color struct {
R, G, B byte
}

注意:结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正的分配内存。

实例化结构体

结构体的实例化是根据结构体的定义的格式创建一份与格式一致的内存区域,结构体实例与实例之间的内存是完全独立的。
结构体本身也是一种类型,可以向整型、字符串一样,以var的方式声明结构体即可完成实例化。
结构体基本实例化格式:

1
var 结构体实例 结构体

用结构体表示的点结构(Point)的实例化过程请参见下面的代码:

1
2
3
4
5
6
7
8
type Point struct {
X int
Y int
}

var point Point
p.X = 10
p.Y = 20

创建指针型结构体

使用new关键字对类型(结构体、整型、字符串等)进行实例化,结构体实例化后会形成指针类型的结构体。
使用new实例化指针型结构体格式:

1
ins := new(T)

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

下面的例子定义了一个玩家(Player)的结构,玩家拥有名字、生命值和魔法值,实例化玩家(Player)结构体后,可对成员进行赋值,代码如下:

1
2
3
4
5
6
7
8
9
10
type Player struct {
Name string
HealthPoint int
MagicPoint int
}

tank := new(Player)
tank.Name = "Mike"
tank.HealthPoint = 300
tank.MagicPoint = 100

###取地址的结构体实例化
在Go语言中,对结构体进行&取地址操作的时候,实际上和对该类型进行一次new实例化操作是一样的效果。
取地址的结构体实例化格式:

1
ins := &T{}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
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"

结构体成员变量初始化

使用“键值对”初始化结构体

结构体键值对的填充是可选的,不需要初始化的字段可以不填入初始化列表中。
结构体实例化字段默认的默认值是字段类型的默认值,比如:数值为0,字符串默认为空字符串,布尔为false,指针为nil等。
1.键值对初始化结构体的书写格式

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

}

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: "我",
},
},
}

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

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

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

多个值使用逗号分隔初始化结构体,格式如下:

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Address struct {
Province string
City string
ZipCode int
PhoneNumber string
}

addr := Address{
"中国",
"北京",
123455,
"0001",
}

fmt.Println(addr)

代码输出:

1
{中国 北京 123455 0001}

初始化匿名结构体

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

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

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
ins := struct {
字段1字段类型1
字段2字段类型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
24
package main

import "fmt"

//打印消息类型,传入匿名结构体
func printMsgType(msg *struct {
id int
data string
}) {
fmt.Printf("%T\n", msg)
}

func main() {
//实例化一个匿名结构体
msg := &struct {
id int
data string
} {
1024,
"hello world"
}

printMsgType(msg)
}

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

结构体方法和接收器

Go 语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收器(Receiver)。
如果将特定类型理解为结构体或“类”时,接收器的概念就类似于其他语言中的 this 或者 self。
在 Go 语言中,接收器的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。

为结构体添加方法

1.面向过程的实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
type Bag struct {
items []int
}

//将一个物品放入背包的过程
func Insert(bag *Bag, itemid int) {
bag.items = append(b.items, itemid)
}

func main() {
bag := new(Bag)
Insert(bag, 10086)
}

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

2. Go语言结构体方法

将背包及放入背包的物品中使用 Go 语言的结构体和方法方式编写:为 *Bag 创建一个方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
type Bag struct {
items []int
}

func (bag *Bag) Insert(itemid int) {
bag.items = append(bag.items, itemid)
}


func main() {
bag := &Bag{}
bag.Insert(10010)
}

(bag *Bag)表示接收器,即Insert作用的实例,每个方法只能有一个接收器:
接收器

接收器(方法作用的目标)

接收器格式:

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

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

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
package main

import "fmt"

//定义一个属性结构
type Property struct {
value int
}

//设置属性
func (p *Property) SetValue(v int) {
p.value = v
}

//获取属性
func (p *Property) getValue() int {
return p.value
}

func main() {
p := new(Property)
p.setValue(100)
value := p.getValue()
fmt.Prinln(value)
}

2.非指针类型接收器
当方法作用于非指针类型接收器时,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)
}

代码输出:

1
{3, 3}

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

任意类型添加方法

为基本类型添加方法

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

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 MyInt int

func (m MyInt) IsZero() bool {
return m == 0
}

func (m MyInt) Add(other int) int {
return other + int(m)
}

func main() {
var b MyInt

fmt.Println(b.IsZero())

b = 1

fmt.Println(b.Add(2))
}

使用事件系统实现事件的响应和处理

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
package main 

import "fmt"

type class struct {}

func (c *class) Do(v int) {
fmt.Println("Call method do:", v)
}

func funcDo(v int) {
fmt.Println("Call method do:", v)
}

func main() {
//声明一个函数回调
var delegate func(int)

c := new(class)
delegate = c.Do
delegate(100)

delegate = funcDo
delegate(100)
}

代码输出:

1
2
call method do: 100
call function do: 100

这段代码能运行的基础在于:无论是普通函数还是结构体的方法,只要它们的签名一致(参数完全一致),与它们签名一致的函数变量就可以保存普通函数或是结构体方法。

事件系统的基本原理

待续…

类型内嵌和结构体内嵌

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

1
2
3
4
5
6
7
8
9
10
11
type Data struct {
int
float32
bool
}

ins := &Data{
int: 10,
float32: 3.14,
bool: true,
}

类型内嵌其实仍然拥有自己的字段名,只是字段名就是其类型本身而已,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
结构体实例化后,如果匿名的字段类型为结构体,那么可以直接访问匿名结构体里的所有成员,这种方式被称为结构体内嵌。

声明结构体内嵌

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
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)
}

结构体内嵌模拟类的继承

在面向对象的思想中,实现对象关系需要使用“继承”特性。例如,人类不能飞行,鸟类可以飞行。人类和鸟类都可以继承自可行走类,但只有鸟类继承自飞行类。
面向对象的设计原则中也建议对象最好不要使用多重继承,有些面向对象语言从语言层面就禁止了多重继承,如 C# 和 Java 语言。鸟类同时继承自可行走类和飞行类,这显然是存在问题的。在面向对象思想中要正确地实现对象的多重特性,只能使用一些精巧的设计来补救。
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
34
35
36
37
38
39
40
41
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 walk")
}

//人类
type Human struct {
Walable
}

//鸟类
type Bird struct {
Walkable
Flying
}

func main() {
//实例化鸟类
b := new(Bird)
fmt.Println("Bird:")
b.Fly()
b.Walk()

//实例化人类
h := new(Human)
fmt.Prinln("Human:")
h.Walk()
}

初始化内嵌结构体

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

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 Wheel struct {
Size int
}

type Engine struct {
Power int
Type string
}

type Car struct {
Wheel
Engine
}

func main() {
c := Car {
Wheel: Wheel {
Size: 18,
},

Engin: Engin {
Type: "1.47",
Power: 143,
},
}

fmt.Println("%+v\n", c)

}