总体来看,go语言中的面向对象在使用方式上是灵活易用的,可以说设计理念真的很先进,让人有一种如沐春风的感觉。
如果你在学生时代经历了一个从c到c++的学习历程,你是否还记得,老师会说c++是面向对象的,所以我们不必再使用c中的结构体作为数据结构。我们只需定义的是c++中的类,因为类中不只有成员属性,也有成员函数。换句话说, class是可以完美替代struct的,而且更强大。
回到go中,我们的面向对象使用的就是struct
,但时代不同了,这次我们的struct也可以有”成员函数”了。
定义一个典型的面向对象方式
package main
import "fmt"
type Human struct {
height float32
weight int
}
func (h Human) BMIindex() (index int){
index = h.weight / int(h.height * h.height)
return
}
func main() {
person := Human{1.83, 75}
fmt.Printf("this person's height is %.2f m\n", person.height)
fmt.Printf("this person's weight is %d kg\n", person.weight)
fmt.Printf("this person's BMI index is %d\n", person.BMIindex())
}
在main函数中我们初始化了一个Human对象,并分别读取了他们的属性height和weight,最后调用了Human对象的成员函数BMIindex(),通过计算获得了这个人的”体质指数”。
Receiver
上述例子中,一个Human对象的成员函数就是通过Receiver来定义的。我们给一个普通的func添加了Receiver(就是上述示例中的h Human),就构成了一个method
BMIindex()。在这种情况下,这个函数只能依赖于一个Human对象来起作用,而不能被独立调用。其正确的调用方式就是上述的person.BMIindex()
下述又一个例子,我们希望通过定义成员函数来改变对象的成员属性。
package main
import "fmt"
type Human struct {
height float32
weight int
}
func (h Human) BMIindex() (index int){
index = h.weight / int(h.height * h.height)
return
}
func (h Human) setHeight(height float32) {
h.height = height
}
func main() {
person := Human{1.83, 75}
fmt.Printf("this person's height is %.2f m\n", person.height)
fmt.Printf("this person's weight is %d kg\n", person.weight)
fmt.Printf("this person's BMI index is %d\n", person.BMIindex())
person.setHeight(1.90)
fmt.Printf("this person's height is %.2f m\n", person.height)
}
输出结果:
this person's height is 1.83 m
this person's weight is 75 kg
this person's BMI index is 25
this person's height is 1.83 m
可以看出,我们调用person.setHeight(1.90)之后,person的height属性并没有改变为1.90。而为了解决这个问题,我们需要改变receiver。我们将setHeight()函数定义为下述形式即可。
func (h *Human) BMIindex() (index int){
index = h.weight / int(h.height * h.height)
return
}
原因:我们将对象的指针类型
作为receiver,才能改变其属性的值。其本质为,我们可以把receiver看作func的独特的一个参数。如果传递的是一个对象类型,那么函数中改变的实际上是这个对象的副本;而如果传递一个指针类型,改变的才是这个对象本身。
关于receiver的选择,可以这样理解,如果需要获取对象属性(get),则选用对象作为receiver;如果需要改变对象属性(set),则选取指针作为receiver。
method的适用范围
上述示例中,我们定义的method都是对应于struct的,但实际上method的定义可以依赖于所有的自定义类型
。所谓自定义类型,就是通过type语句给一些内置类型起了个”别名”后所定义的新类型。
package main
import "fmt"
type Sex string
func (s *Sex) change(){
if *s == Sex("女") {
*s = Sex("男")
}
}
func main() {
sex := Sex("女")
fmt.Println("previous sex is ", sex)
sex.change()
fmt.Println("current sex is ", sex)
}
这里,我们新定义了一个类型Sex,并且为其定义了一个method change()。
面向对象中的继承
package main
import "fmt"
type Human struct {
height float32
weight int
}
type Woman struct {
Human
sex string
}
func (h Human) BMIindex() (index int){
index = h.weight / int(h.height * h.height)
return
}
func main() {
woman := Woman{Human{1.65, 50}, "女"}
fmt.Printf("this woman's height is %.2f m\n", woman.height)
fmt.Printf("this woman's wight is %d kg\n", woman.weight)
fmt.Printf("this woman's BMIindex is %d\n", woman.BMIindex())
}
这个例子展现了Woman对Human的继承。在Woman结构体中包含了匿名字段
Human,那么Human中包含的属性也是属于一个Woman对象的属性,Human对象的method同样也可以被Woman对象所使用。值得注意的是,这个method可以被重写
。只需要再定义一个以Woman对象类型为receiver的BMIindex(),那么再次调用woman.BMIindex时,实际调用的函数是新定义的这个函数。这就是method的重写。
访问属性
如果你对面向对象中的访问属性很熟悉的话,你一定知道public、private和protected作为访问修饰符的作用。而在go语言中,我们使用大小写
来区分。
标识符首字母大写,相当于public属性。这样的成员属性或成员函数可以被在包外部
被调用。例如上述Woman、BMIindex。
标识符首字母小写,相当于protected属性。这样的成员属性或成员函数只能在包内部
被使用。