要实现面向对象编程,就必须实现面向对象编程的三大特性:封装、继承和多态。
封装
将函数定义为归属某个自定义类型,这就等同于实现了类的成员方法,如果这个自定义类型是基于结构体的,那么结构体的字段可以看做是类的属性。
package main
import (
"fmt"
)
type Student struct {
id uint
name string
male bool
score float64
}
func NewStudent(id uint, name string, score float64) Student {
return Student{id: id, name:name, score:score}
}
func (s Student) GetName() (string,uint) {
return s.name ,s.id
}
func (s *Student) SetName(name string) {
s.name = name
}
func main() {
student := NewStudent(1, "学院君", 100)
//fmt.Println(student)
student.SetName("学院军小号")
(&student).SetName("学院君小小小号")
a,b:=student.GetName()
fmt.Println("name",a,b)
}
值方法和指针方法的区别
另外,需要声明的是,在 Go 语言中,当我们将成员方法 SetName 所属的类型声明为指针类型时,严格来说,该方法并不属于 Student 类,而是属于指向 Student 的指针类型,所以,归属于 Student 的成员方法只是 Student 类型下所有可用成员方法的子集,归属于 *Student 的成员方法才是 Student 类完整可用方法的集合。
我们在调用方法时,之所以可以直接在 student 实例上调用 SetName 方法,是因为 Go 语言底层会自动将 student 转化为对应的指针类型 &student,所以真正调用的代码是 (&student).SetName(“学院君小号”),这一点需要大家知晓。
总结下来,就是一个自定义数据类型的方法集合中仅会包含它的所有「值方法」,而该类型对应的指针类型包含的方法集合才囊括了该类型的所有方法,包括所有「值方法」和「指针方法」,指针方法可以修改所属类型的属性值,而值方法则不能。
小结:
我们来简单总结下,在 Go 语言中,有意弱化了传统面向对象编程中的类概念,这也符合 Go 语言的简单设计哲学,基于结构体定义的「类」就是和内置的数据类型一样的普通数据类型而已,内置的数据类型也可以通过 type 关键字转化为可以包含自定义成员方法的「类」。
一个数据类型关联的所有方法,共同组成了该类型的方法集合,和其他支持面向对象编程的语言一样,同一个方法集合中的方法也不能出现重名,并且,如果它们所属的是一个结构体类型,那么它们的名称与该类型中任何字段的名称也不能重复。
继承
Go 虽然没有直接提供继承相关的语法实现,但是我们通过组合的方式间接实现类似功能,所谓组合,就是将一个类型嵌入到另一个类型,从而构建新的类型结构。(通过组合来代替继承提高类的扩展性)
我们来看一个例子,现在有一个 Animal 结构体类型,它有一个属性 Name 用于表示该动物的名称,以及三个成员方法,分别用来获取动物叫声、喜欢的食物和动物的名称:
type Animal struct {
Name string
}
func (a Animal) Call() string {
return "动物的叫声..."
}
func (a Animal) FavorFood() string {
return "爱吃的食物..."
}
func (a Animal) GetName() string {
return a.Name
}
如果我们要定义一个继承自该类型的子类 Dog,可以这么做:
type Dog struct {
Animal
}
这里,我们在 Dog 结构体类型中,嵌入了 Animal 这个类型,这样一来,我们就可以在 Dog 实例上访问所有 Animal 类型包含的属性和方法:
func main() {
animal := Animal{"中华田园犬"}
dog := Dog{animal}
fmt.Println(dog.GetName())
fmt.Println(dog.Call())
fmt.Println(dog.FavorFood())
}
上述代码的打印结果如下:
中华田园犬
动物的叫声...
爱吃的食物...
多态
此外,我们还可以通过在子类中定义同名方法来覆盖父类方法的实现,在面向对象编程中这一术语叫做方法重写,比如在上述 Dog 类型中,我们可以重写 Call 方法和 FavorFood 方法的实现如下:
func (d Dog) FavorFood() string {
return "骨头"
}
func (d Dog) Call() string {
return "汪汪汪"
}
当我们再执行 main 函数时,直接在 Dog 实例上调用 Call 方法或 FavorFood 方法时,调用的就是 Dog 类中定义的方法而不是 Animal 中定义的方法:
中华田园犬
汪汪汪
骨头
当然,你可以可以像这样继续调用父类 Animal 中的方法:
fmt.Print(dog.Animal.Call())
fmt.Println(dog.Call())
fmt.Print(dog.Animal.FavorFood())
fmt.Println(dog.FavorFood())
只不过 Go 语言不同于 Java、PHP 等面向对象编程语言,没有专门提供引用父类实例的关键字罢了(super
、parent
等),在 Go 语言中,设计哲学一切从简,没有一个多余的关键字,所有的调用都是所见即所得。
这种同一个方法在不同情况下具有不同的表现方式,就是多态,在传统面向对象编程中,多态还有另一个非常常见的使用场景 —— 类对接口的实现,Go 语言也支持此功能,关于这一块我们放到后面接口部分单独介绍。
继承指针类型的属性和方法
当然,在 Go 语言中,你还可以以指针方式继承某个类型的属性和方法:
type Dog struct {
*Animal
}
这种情况下,除了传入 Animal 实例的时候要传入指针引用之外,其它调用无需修改:
func main() {
animal := Animal{"中华田园犬"}
pet := Pet{"宠物狗"}
dog := Dog{&animal, pet}
fmt.Println(dog.Animal.GetName())
fmt.Print(dog.Animal.Call())
fmt.Println(dog.Call())
fmt.Print(dog.Animal.FavorFood())
fmt.Println(dog.FavorFood())
}
当我们通过组合实现类之间的继承时,由于结构体实例本身是值类型,如果传入值字面量的话,实际上传入的是结构体实例的副本,对内存耗费更大,所以组合指针类型性能更好。
为组合类型设置别名
前面的示例调用父类方法时都直接引用的是组合类型(父类)的类型字面量,其实,我们还可以像基本类型一样,为其设置别名,方便引用:
type Dog struct {
animal *Animal
pet Pet
}
...
func main() {
animal := Animal{"中华田园犬"}
pet := Pet{"宠物狗"}
dog := Dog{&animal, pet}
// 通过 animal 引用 Animal 类型实例
fmt.Println(dog.animal.GetName())
fmt.Print(dog.animal.Call())
fmt.Println(dog.Call())
fmt.Print(dog.animal.FavorFood())
fmt.Println(dog.FavorFood())
}