Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念,它通过结构体的内嵌再配合接口比一般面向对象具有更高的扩展性和灵活性。

结构体

Go语言中的结构体是一种自定义数据类型,它可以用来封装多个基本数据类型。结构体的英文名称叫struct,与C语言一样,也是通过struct来定义结构体。

Go语言通过结构体来实现面向对象。

定义

使用type和struct关键字来定义结构体,具体代码格式如下:

1
2
3
4
5
type 类型名 struct {
字段名 字段类型
字段名 字段类型

}

举个例子

1
2
3
4
5
type person struct {
name string
city string
age int8
}

跟以往一样,相同类型字段名可以写在同一行

1
2
3
4
type person struct {
name, city string
age int8
}

语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型。

实例化

当结构体实例化的时候,才会真正分配内存,且才能使用结构体的字段(类似类与对象)

1
var 结构体实例名 结构体

基本实例化

沿用我们的person结构体

1
2
3
4
5
6
func main() {
var p1 person
p1.name = "IceWindy"
p1.city = "东莞"
p1.age = 18
}

Go语言访问结构体实例的字段,使用的是类似面向对象语言中的.操作符,而不是C语言中的->

匿名结构体

有些时候我们仅需要临时的定义一些数据结构,这时候就可以用到匿名结构体

1
2
3
4
5
var 结构体实例 struct {
字段名 字段类型
字段名 字段类型

}

指针类型结构体

我们可以使用new关键字对结构体进行实例化,这种实例化得到的是和结构体的地址

1
2
3
var p2 = new(person)
fmt.Printf("%T\n", p2) //output: *main.person
fmt.Printf("p2=%#v\n", p2) //output: p2=&main.person{name:"", city:"", age:0}

从终端打印的结果我们可以看出,p2是一个结构体指针。

值得注意的是,结构体指针是可以通过.直接访问结构体实例的字段的

1
2
var p2 = new(person)
p2.name = "1ce_Windy"

取结构体的地址实例化

使用&对结构体进行取地址操作与new关键字实例化结构体是一样的效果,得到的就是一个结构体指针

1
2
3
var p3 = new(person)
fmt.Printf("%T\n", p3) //output: *main.person
fmt.Printf("p2=%#v\n", p3) //output: p3=&main.person{name:"", city:"", age:0}

初始化

从上面的例子我们已经看出没有初始化的结构体,内部字段均为零值。

使用键值对初始化

1
2
3
4
5
6
p3 := person{
name: "IceWindy",
city: "东莞",
age: 18,
}
fmt.Printf("p3=%#v\n", p3) //output: p3=main.person{name:"IceWindy", city:"东莞", age:18}

当某些字段暂时不需要初始值时,可以省略不写。没有指定初始值的字段的值为零值。

同样的方法也可以对&初始化的结构体指针使用

使用值的列表初始化

1
2
3
4
5
6
p4 := person{
"IceWindy",
"东莞",
18,
}
fmt.Printf("p4=%#v\n", p4) //output: p4=main.person{name:"IceWindy", city:"东莞", age:18}

这种方法的初始化有几点需要注意

  • 必须初始化结构体内的所有字段
  • 初始值的顺序需与结构体声明顺序一致
  • 不能与键值对初始化方式混用

构造函数

Go语言的结构体是没有构造函数的,如果有需要我们可以自行实现。

由于struct的实例是值类型,如果结构体比较复杂的话,值拷贝的性能开销会很大,因此可以通过返回结构体指针的方式,减少性能开销

1
2
3
4
5
6
7
func newPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}

方法与接收者

Go语言中的方法是一种作用于特定类型变量的函数。特定类型变量被称作为接收者

接收者这个概念类似于其他语言中的this。方法的定义格式如下

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

举个简单的例子

1
2
3
4
5
6
7
8
9
10
11
func (p person) personSay() {
fmt.Printf("%s say:Hello World\n", p.name)
}

func main() {
p1 := person{
name: "IceWindy",
age: 18,
}
p1.personSay()
}

指针类型的接收者

指针类型的接收者由一个结构体的指针组成。

由于指针的特性,调用方法修改时可以修改接收者指针的任意成员变量,在方法结束后,修改都可以得以保留。这种方法更接近于其他语言中面向对象中的this,例如修改结构体内字段等

1
2
3
func (p *person) SetName(name string) {
p.name = name
}

值类型的接收者

当方法作用于值类型接收者时,相当于执行方法时,会克隆一份副本出来,因此任何修改都会仅针对副本,无法修改接收者变量本身

1
2
3
4
5
6
7
8
9
10
func (p Person) SetAge(age int8) {
p.age = age
}

func main() {
p1 := NewPerson("IceWindy", 18)
fmt.Println(p1.age) // output:18
p1.SetAge2(19)
fmt.Println(p1.age) // output:18
}

什么时候应该使用指针类型接收者

  1. 需要修改接收者中的值
  2. 接收者是拷贝代价比较大的大对象
  3. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

任意类型添加方法

不仅是结构体可以添加方法,在Go语言中,方法的接收者可以是任意类型。

举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

1
2
3
4
5
type NewInt int

func (i NewInt) SayHello() {
fmt.Printf("Hello NewInt")
}

**注意:**不能给别的包的类型定义方法

匿名字段

Go语言的结构体允许成员字段在声明时没有字段名只有类型。但并不是代表真正不是没有字段名,而是用类型名作为字段名,因此一个结构体中同类型的匿名字段只能有一个

1
2
3
4
5
6
7
8
9
10
11
12
type student struct {
string
int
}

func main() {
s1 := student{
"IceWindy",
18,
}
fmt.Println(s1.string, s1.string)
}

嵌套结构体

一个结构体中可以嵌套包含另一个结构体或结构体指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Person struct {
name string
age int8
}

type Student struct {
person Person
studentID int
}

func main() {
stu1 := Student{
person: Person{
name: "IceWindy",
age: 18,
},
studentID: 114514,
}
fmt.Printf("stu1=%#v\n", stu1) //output: stu1=main.Student{Person:main.Person{name:"IceWindy", age:18}, studentID:114514}
}

嵌套匿名字段

上面嵌套结构体中的Person结构体也可以采用匿名字段的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Person struct {
name string
age int8
}

type Student struct {
Person
studentID int
}

func main() {
stu1 := Student{
Person: Person{
name: "IceWindy",
age: 18,
},
studentID: 114514,
}
fmt.Printf("stu1=%#v\n", stu1) //output: stu1=main.Student{Person:main.Person{name:"IceWindy", age:18}, studentID:114514}
}

嵌套结构体的字段名冲突

当嵌套结构体内部采用了相同的字段名,我们需要指定具体嵌套结构体的字段名

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
//Address 地址结构体
type Address struct {
Province string
City string
CreateTime string
}

//Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}

//User 用户结构体
type User struct {
Name string
Gender string
Address
Email
}

func main() {
var user User
user.Name = "IceWindy"
user.Gender = "男"
user.Address.CreateTime = "1145"
user.Email.CreateTime = "1145"
}

结构体的“继承”

使用嵌套结构体的方式,可以实现其他面向对象编程语言中的继承

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
//Animal 动物
type Animal struct {
name string
}

func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}

//Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
}

func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}

func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal{ //注意嵌套的是结构体指针
name: "乐乐",
},
}
d1.wang() //乐乐会汪汪汪~
d1.move() //乐乐会动!
}

字段的可见性

结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。

JSON序列化

JSON序列化就是将Go语言中的数据(例如结构体实例等)转换为满足JSON格式的数据

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
type student struct {
StudentID int
StudentName string
}

type class struct {
ClassName string
Student []student
}

// Student构造函数
func newStudent(sID int, sName string) student {
return student{
StudentID: sID,
StudentName: sName,
}
}

func main() {
//结构体内需要首字母大写,即对外包开放
//无首字母大写的字段无法被JSON包访问
c1 := class{
ClassName: "101班",
//切片需要初始化
Student: make([]student, 0, 20),
}
for i := 0; i < 10; i++ {
tempStu := newStudent(i, fmt.Sprintf("Stu%02d", i))
c1.Student = append(c1.Student, tempStu)
}
fmt.Printf("%T\n", c1)
fmt.Printf("%#v\n", c1)

//JSON序列化
data, err := json.Marshal(c1)
if err != nil {
fmt.Println("JSON序列化失败")
return
}
fmt.Printf("%T\n", data) //output:[]uint8,即[]byte
fmt.Println(string(data)) //为了便于打印,需要转换为string
/* 经过格式化之后的结果(略作缩进)
{
"ClassName":"101班",
"Student":[
{"StudentID":0,"StudentName":"Stu00"},
{"StudentID":1,"StudentName":"Stu01"},
{"StudentID":2,"StudentName":"Stu2"},
{"StudentID":3,"StudentName":"Stu03"},
{"StudentID":4,"StudentName":"Stu04"},
{"StudentID":5,"StudentName":"Stu05"},
{"StudentID":6,"StudentName":"Stu06"},
{"StudentID":7,"StudentName":"Stu07"},
{"StudentID":8,"StudentName":"Stu08"},
{"StudentID":9,"StudentName":"Stu09"}
]
}
*/

//JSON反序列化:JSON字符串转换为Go语言的类型
jsonStu := "{\"ClassName\":\"101班\",\"Student\":[{\"StudentID\":0,\"StudentName\":\"Stu00\"},{\"StudentID\":1,\"StudentName\":\"Stu01\"}]}"
c2 := class{}
//传入一个结构体指针使得可以修改结构体的内容
err = json.Unmarshal([]byte(jsonStu), &c2)
if err != nil {
fmt.Printf("反序列化失败")
return
}
fmt.Printf("%#v\n", c2)
}

结构体标签(Tag)

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag在结构体字段的后方定义,由一对反引号包裹起来

1
`key1:"value1" key2:"value2"`

结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔,因此不要在键值之间使用空格分隔。

以下以json tag修改序列化后key的名称为例子展示Tag的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Stu struct {
ID int `json:"id"` //通过json tag实现json序列化时得到与默认字段名不同的key
Name string `json:"name"`
Class string //默认使用字段名
}

func main() {
s1 := Stu{
ID: 1,
Name: "IceWindy",
Class: "101班",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Printf("json marshal failed!")
return
}
fmt.Printf("%s\n", data) //output:{"id":1,"name":"IceWindy","Class":"101班"}
}