抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >
  • struct结构体可以说是Go语言中的最重要的组成部分之一

  • Go不是传统的面向对象和面向过程的语言,如果确切地说可以说是面向结构的语言

  • 掌握好struct的知识点是非常有必要的,同时,利用好strcut也能写出面向对象思想的代码

Github issues:https://github.com/littlejoyo/Blog/issues/

1.什么是结构体?

  • Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,对应的关键字叫struct

  • 当Go提供的基本类型满足不了我们想要表示的属性,就可以通过struct来定义自己的类型

  • Go通过定义数据结构的方式,实现了与类似Class(类)的功能,同样具备了自己定义和实例化的方式。

2.结构体的定义和初始化

2.1 结构体的定义

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

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

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

2.2 结构体实例化

  • 只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。

  • 结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。当然也可以直接使用:=符号进行结构体的创建

1
var 结构体实例 结构体类型

一个实例化栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type person struct {
name string
city string
age int8
}

func main() {
var p1 person
p1.name = "Joyo"
p1.city = "Beijing"
p1.age = 18
fmt.Printf("p1=%v\n", p1) //p1={Joyo Beijing 18}
fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"Joyo", city:"Beijing", age:18}
}

这样我们就拥有了一个person的自定义类型,它有name、city、age三个字段,分别表示姓名、城市和年龄。然后通过使用这个person结构体就能够很方便的在程序中表示和存储有关于人的信息。

续上,另一种实例化方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
p2 := person{
name: "joyo",
city: "Beijing",
age: 18,
}
fmt.Printf("p1=%v\n", p1) //p1={Joyo Beijing 18}
fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"Joyo", city:"Beijing", age:18}

fmt.Printf("name=%s\n", p.name) // name=Joyo
fmt.Printf("name=%s\n", p.city) // city=Beijing
fmt.Printf("name=%d\n", p.age) // age=18
}

注意:

  1. 上面最后一个逗号”,”不能省略,Go会报错,这个逗号有助于我们去扩展这个结构,所以习惯后,这是一个很好的特性。

  2. 结构体实例化后,就可以通过.来访问结构体的字段(成员变量),例如上面的p1.namep1.age等。

  3. 上面实例化结构体的时候,也可以不指定属性直接赋值,但是这样看起来不够直观,一般还是建议使用指定的方式。

1
p3 := person{"Joyo","Beijing",18}
  1. 一次性无法将结构体的属性进行赋值的话,还可以通过先赋值部分,再通过.来追加赋值完整属性的对象
1
2
3
4
5
6
7
8
9
func main(){
p4 := person{
name: "Joyo",
city: "Beijing",
}
p4.age = 18

fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"Joyo", city:"Beijing", age:18}
}

2.3 匿名结构体

在定义一些临时数据结构等场景下还可以使用匿名结构体。

同样可以使用上面的两种方式进行实例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
// 方式1:
var p1 struct{Name string; Age int}
p1.Name = "Joyo"
p1.Age = 18
fmt.Printf("%#v\n", user)//struct { Name string; Age int }{Name:"Joyo", Age:18}

// 方式2:
p2 := struct {
Name string
Age int
}{Name: "Tom", Age: 18}

fmt.Printf("%#v\n", p2)//struct { Name string; Age int }{Name:"Tom", Age:18}
}

2.4 创建指针类型结构体

可以通过使用new关键字或者&符号对结构体进行实例化,得到的是结构体的内存地址,即是指针地址。

事实上使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。

格式如下:

1
2
3
4
5
6
7
8
9
func main(){
var p1 = new(person)
fmt.Printf("%T\n", p1) //*main.person(打印类型)
fmt.Printf("p1=%#v\n", p1) //p1=&main.person{name:"", city:"", age:0}

p2 := &person{}
fmt.Printf("%T\n", p2) //*main.person(打印类型)
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}
}

从打印的结果中我们可以看出p1p2的类型都是一个结构体指针,需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员。

1
2
3
4
5
var p = new(person)
p.name = "Joyo"
p.age = 18
p.city = "Beijing"
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"小王子", city:"上海", age:28}

p.name = “Joyo”其实在底层是(*p).name = “Joyo”,这是Go语言帮我们实现的语法糖。

2.5 分析指针类型结构体

上面列出了两种实例化的方式分别是:

1
2
p1 := person{name:"Joyo",city:"Beijing",age:18}
p2 := &person{name:"Joyo",city:"Beijing",age:18}

这两种赋值方式,有何不同?

  • 第一种将整个数据结构赋值给变量p1,p1从此变成person的实例;

  • 第二种使用了一个特殊符号&在数据结构前面,它表示返回这个数据结构的引用,也就是这个数据结构的地址,所以p2也指向这个数据结构。

那p1和p2都指向这个数据结构,有什么区别?

实际上,赋值给p1的是person实例的地址,赋值给p2是一个中间的指针,这个指针里保存了person实例的地址。它们的关系相当于:

1
2
p1 -> person{}
p1 -> Pointer -> person{}

其中Pointer在内存中占用一个长度为一个机器字长的单独数据块,64位机器上一个机器字长是8字节,所以赋值给p2的这个8字节长度的指针地址,这个指针地址再指向person{},而p2则是直接指向person{}

2.6 结构体初始化

没有初始化的结构体,其成员变量都是对应基本数据类型的零值。

1
2
3
4
5
6
7
8
9
10
type person struct {
name string
city string
age int8
}

func main() {
var p person
fmt.Printf("p=%#v\n", p) //p=main.person{name:"", city:"", age:0}
}

使用键值初始化:

使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。

1
2
3
4
5
6
p1 := person{
name: "Joyo",
city: "Beijing",
age: 18,
}
fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"小王子", city:"Beijing", age:18}

也可以对结构体指针进行键值对初始化,例如:

1
2
3
4
5
6
p2 := &person{
name: "Joyo",
city: "Beijing",
age: 18,
}
fmt.Printf("p2=%#v\n", p2) //p2=main.person{name:"小王子", city:"Beijing", age:18}

当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。

1
2
3
4
p3 := &person{
name: "Joyo",
}
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"Joyo", city:"", age:0}

使用值的列表初始化:

初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:

1
2
3
4
5
6
p := &person{
"Joyo",
"Beijing",
18,
}
fmt.Printf("p=%#v\n", p) //p=&main.person{name:"Joyo", city:"Beijing", age:18}

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

  1. 必须初始化结构体的所有字段。
  2. 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  3. 该方式不能和键值初始化方式混用。

2.7 空结构体

  • struct{}表示为空结构体,注意空结构体是不占用空间的。
1
2
var v struct{}
fmt.Println(unsafe.Sizeof(v)) // 0

3.方法和接收者

3.1 认识方法和接受者

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self

方法的定义格式如下:

1
2
3
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是selfthis之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。

  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。

  • 方法名、参数列表、返回参数:具体格式与函数定义相同。

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

//Sing Person唱歌的方法
func (p Person) Sing() {
fmt.Printf("%s开始唱歌了!\n", p.name)
}

func main() {
p1 := &Person{
name:"Joyo",
age:18,
}
p1.Sing()
}

方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。

3.2 指针类型(引用类型)接收者

指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的,会将接收者的属性进行改变。

这种方式就十分接近于其他语言中面向对象中的this或者self。 例如我们为Person添加一个SetAge方法,来修改实例变量的年龄。

1
2
3
4
5
// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
p.age = newAge
}

调用SetAge方法进行属性的修改

1
2
3
4
5
6
7
8
9
func main() {
p1 := &Person{
name:"Joyo",
age:18,
}
fmt.Println(p1.age) // 18
p1.SetAge(20)
fmt.Println(p1.age) // 20 (从18修改为20)
}

3.3 值类型接收者

当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。

在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SetAge 设置p的年龄
// 使用值接收者
func (p Person) SetAge(newAge int8) {
p.age = newAge
}

func main() {
p1 := &Person{
name:"Joyo",
age:18,
}
fmt.Println(p1.age) // 18
p1.SetAge(20) // (*p1).SetAge2(30)
fmt.Println(p1.age) // 18
}

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

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

大多数时候,传递给函数使用指针类型,但极少数时候也有需求直接使用值类型。

3.5 任意类型添加方法

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
//MyInt 将int定义为自定义MyInt类型
type MyInt int

//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是int类型!")
}
func main() {
var m1 MyInt
m1.SayHello() //Hello, 我是int类型!
m1 = 100
fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
}

注意:非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。

4.结构体的匿名字段

4.1 什么是匿名字段?

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Person 结构体Person类型
type Person struct {
string
int
}

func main() {
p1 := Person{
"Joyo",
18,
}
fmt.Printf("%#v\n", p1) //main.Person{string:"Joyo", int:18}
fmt.Println(p1.string, p1.int) //Joyo 18
}

匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个,例如上面的string类型只能有一种,否则会出现同名错误。

4.2 嵌套结构体

一个结构体中可以嵌套包含另一个结构体或结构体指针,同样可以应用上面的匿名字段特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Address 地址结构体
type Address struct {
Province string
City string
}

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

func main() {
u1 := User{
Name: "Joyo",
Gender: "男",
Address: Address{
Province: "广东",
City: "深圳",
},
}
fmt.Printf("u1=%#v\n", u1)//u1=main.User{Name:"Joyo", Gender:"男", Address:main.Address{Province:"广东", City:"深圳"}}
}

嵌套匿名结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Address 地址结构体
type Address struct {
Province string
City string
}

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

func main() {
var u2 User
u2.Name = "Joyo"
u2.Gender = "男"
u2.Address.Province = "广东" //通过匿名结构体.字段名访问
u2.City = "深圳" //直接访问匿名结构体的字段名
fmt.Printf("u2=%#v\n", u2) //u2=main.User{Name:"Joyo", Gender:"男", Address:main.Address{Province:"广东", City:"深圳"}}
}

当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。

4.3 嵌套结构体的字段名冲突

嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。

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
//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 u1 User
u1.Name = "Joyo"
u1.Gender = "男"
// u1.CreateTime = "2019" //ambiguous selector user3.CreateTime
u1.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
u1.Email.CreateTime = "2000" //指定Email结构体中的CreateTime
}

5.利用结构体实现面向对象特性

5.1 构造函数

面向对象中有构造器(也称为构造方法),可以根据类构造出类的实例:对象。

Go虽然不支持面向对象,没有构造函数的概念,但也具有构造函数的功能,毕竟构造函数只是一个方法而已。只要一个函数能够根据数据结构返回这个数据结构的一个实例对象,就可以称之为”构造函数”。

举个栗子:

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

因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型,当然也可以直接返回非引用类型。

调用构造函数生成实例对象:

1
2
p := newPerson("Joyo", "Beijing", 18)
fmt.Printf("%#v\n", p) //&main.person{name:"张三", city:"沙河", age:90}

如果感觉上面的方式太过麻烦,其实可以直接使用Go内置的new()函数用于为一个数据结构分配内存。其中new(x)等价于&x{},以下两语句等价:

1
2
p1 := new(person)
p2 := &person{}

5.2 扩展结构体属性实现继承特性

Go可以通过组合的思想来实现面向对象的继承特性

比如,在前面出现的结构体的数据结构中的字段数据类型都是简简单单的内置类型:stringint。但数据结构中的字段可以更复杂,例如可以是maparray等,还可以是自定义的数据类型

通过传入自定义的数据类型实现继承,使用动物的关系来举个栗子:

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: "Tom",
},
}
d1.wang() //Tom会汪汪汪~
d1.move() //Tom会动!
}

通过组合的方式扩展了结构体的字段,同时也可以实现面向对象继承特性.

5.3 重写

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
//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) move() {
fmt.Printf("%s用四条腿跑起来了!\n", d.name)
}

func main() {
dog := &Dog{
Feet: 4,
Animal: &Animal{ // 注意此行
name: "Tom",
},
}
dog.move() // Tom用四条腿跑起来了!
}

微信公众号

扫一扫关注Joyo说公众号,共同学习和研究开发技术。

weixin-a

评论