Go面向对象之结构体和方法

go语言只支持封装, 不支持继承和多态. Java是典型的支持面向对象的语言之一, 他本身支持面向对象的三大特征, 封装继承和多态; 到了Python中, Python显式的支持封装和继承, Python多态的特性大家争论不休, 虽然Python没有显式的多态语法, 但是由于Python这种动态语言的设计, 他本身就是多态的; 到了Go这里, 却只支持封装了, 与其说Go语言是一种面向对象的语言, 倒不如说他是一种面向接口的语言

go语言没有class, 只有struct, Go语言中也没有构造函数的说法

定义结构体

在结构体中, 可以为不同的key定义不同的数据类型, 结构体是由一系列具有相同类型或不同类型的数据构成的数据集合

结构体更类似于在传统的分层开发领域中的module层(实体层), 一般情况下, 实体层中的每个成员属性都对应了数据库中数据表的一个字段, 实力层存在的意义在于以对象的形式进行传值. 拿三层开发来说, 一般分为表示层/接口层, 业务逻辑层, 数据访问层. 每层与每层之间的数据流转正是通过引入的实体层, 实例化实体层的对象来实现的. 结构体之于Go, 类似于实体层中的类之于Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

type employee struct {
id int
name string
age int
department string
city string

}

func main() {
e := employee{id:123, name:"larry", age:28, department:"paas", city:"TangShan"}
ee := employee{id:234, name:"sam", age:45, department:"daas", city:"BeiJing"}
fmt.Println(e)
fmt.Println(ee)
}

执行结果:

1
2
{123 larry 28 paas TangShan}
{234 sam 45 daas BeiJing}

访问结构体成员

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

import "fmt"

type employee struct {
id int
name string
age int
department string
city string

}

func main() {
var e1 employee // 声明e1变量为employee类型
var e2 employee // 声明e2变量为employee类型

// 为e1对象赋值
e1.id = 123
e1.name = "larry"
e1.age = 28
e1.department = "paas"
e1.city = "TangShan"

// 为e2对象赋值
e2.id = 234
e2.name = "sam"
e2.age = 45
e2.department = "daas"
e2.city = "BeiJing"

// 访问结构体对象的成员
fmt.Println(e1.name, e1.age)
fmt.Println(e2.name, e2.age)
}

执行结果:

1
2
larry 28
sam 45

结构体作为函数参数

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"

type employee struct {
id int
name string
age int
department string
city string

}

func printEmpInfo(emp employee) {
fmt.Println(emp.name, emp.city)
}

func main() {
e := employee{id:123, name:"larry", age:28, department:"paas", city:"TangShan"}
ee := employee{id:234, name:"sam", age:45, department:"daas", city:"BeiJing"}

printEmpInfo(e)
printEmpInfo(ee)
}

结构体用作函数参数的时候, 最像三层开发中的实体层用法

结构体指针

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

import "fmt"

type employee struct {
id int
name string
age int
department string
city string

}

func main() {
e := employee{id:123, name:"larry", age:28, department:"paas", city:"TangShan"}

var e1 *employee

e1 = &e

fmt.Println(e1.age, e.age)

e1.age = 29

fmt.Println(e1.age, e.age) // 由于e1对象拿到的是e对象的指针, 所以对任意一个变量赋值都会被更改
}

执行结果:

1
2
28 28
29 29

方法

在Python中, 方法和函数的区别可以简单理解为在类中的函数就叫方法. 在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 employee struct {
id int
name string
age int
department string
city string

}

// 定义结构体方法
// (emp employee) 在函数名的前面定义了接收者, 类似于其他语言的this, 或是Python的self
func (emp employee) printEmpInfo(arg string) {
fmt.Printf("我是形式参数 %s\n", arg)
fmt.Println(emp.name)
fmt.Println(emp.age)
}

func main() {
e := employee{id:123, name:"larry", age:28, department:"paas", city:"TangShan"}
e.printEmpInfo("PolarSnow")
}

执行结果:

1
2
3
我是形式参数 PolarSnow
larry
28

上面的demo中, e.printEmpInfo("PolarSnow") e对象调用了printEmpInfo方法, 并显式传递了PolarSnow参数, 其实在背后, 在存在一个隐式传参, 就是e把自己传递给了方法定义中的(emp employee), 类似于Java中的this, 和Python中的self

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"

type employee struct {
id int
name string
age int
department string
city string

}

// 定义普通函数
func printEmpInfo(emp employee, arg string) {
fmt.Printf("我是形式参数 %s\n", arg)
fmt.Println(emp.name)
fmt.Println(emp.age)
}

func main() {
e := employee{id:123, name:"larry", age:28, department:"paas", city:"TangShan"}
printEmpInfo(e, "PolarSnow")
}

这么写效果是一样的, 只是调用的时候是直接调用函数, 把结构体对象当做形式参数传递, 而不是调用结构体自己的方法, 将自己隐式传递到方法中

结构体方法中隐式传参的问题

注意: 默认情况下, Go语言所有参数都是传值, func (emp employee) printEmpInfo(arg string) 也不例外, 同样是值传递

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"

type employee struct {
id int
name string
age int
department string
city string

}

// 定义结构体方法
func (emp employee) setEmpAge(age int) {
emp.age = age
fmt.Println("我是结构体方法里的值", emp.age)
}

func main() {
e := employee{id:123, name:"larry", age:28, department:"paas", city:"TangShan"}
e.setEmpAge(29)
fmt.Println("我是结构体方法外面的值", e.age)
}

执行结果:

1
2
我是结构体方法里的值 29
我是结构体方法外面的值 28
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
package main

import "fmt"

type employee struct {
id int
name string
age int
department string
city string

}

// 定义结构体方法
// 只要将接收结构体对象的数据类型前加*(取指针) 则方法内对结构体对象成员的修改就会同步到方法外部
func (emp *employee) setEmpAge(age int) {
emp.age = age
fmt.Println("我是结构体方法里的值", emp.age)
}

func main() {
e := employee{id:123, name:"larry", age:28, department:"paas", city:"TangShan"}
// 调用方不需要任何更改, Go会自动去识别是应该传值还是传址
e.setEmpAge(29)
fmt.Println("我是结构体方法外面的值", e.age)
}

执行结果:

1
2
我是结构体方法里的值 29
我是结构体方法外面的值 29

注意: 只有指针才可以改变结构体中的内容

nil指针也是可以调用方法的

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"

type employee struct {
id int
name string
age int
department string
city string

}

func (emp *employee) setEmpAge(age int) {
if emp == nil {
fmt.Println("employee 对象为 nil 指针")
return
}
}

func main() {
var e *employee
e.setEmpAge(29)
}

执行结果:

1
employee 对象为 nil 指针

值接收者 vs 指针接收者

  • 要改变内容必须使用指针接收者
  • 结构过大也需要考虑使用指针接受者
  • 一致性: 如果有指针接收者, 最好都是指针接收者