Go语言鸭子类型“面向对象”编程
介绍鸭子类型
我们知道Go语言没有class,也就意味着Go语言没有类和对象,也就无法做到真正意义上的面向对象编程。而面向对象编程非常重要的几个特性封装、继承、重载、多态
。其中最重要的两个特性当属继承和多态了。继承可以实现类之间的抽象关系,多态保证了继承以后,可以实现更丰富的功能。
Go语言通过“鸭子类型”的方式,也能实现继承
和多态
。所谓鸭子类型
就是[1]:
当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子.
封装与继承
在Go语言中通过结构体的嵌套来模拟继承,结构体相当于封装了类的属性,结构体的方法,相当于类的方法,于是就实现了面向对象的封装和继承。
type Animal struct {
Name string
Age uint8
}
func (an *Animal) Cry() {
fmt.Println(an.Name, ",I can cry!")
}
type Duck struct {
Animal
Type string // 简单分类,卵生动物
}
func (duck *Duck) Cry() {
fmt.Println(duck.Name, ",I can Cry Ga Ga!")
}
type Dog struct {
Animal
Type string // 哺乳动物
}
func (dog *Dog) Cry() {
fmt.Println(dog.Name, ",I can cry Wow Wow!")
}
封装
通过Set
和Get
方法提供属性的修改和访问,方便保护属性的安全。把属性定义小写字母开头,不支持导出即可,这样在别的包使用时,只能通过Set
和Get
方法,而不能直接访问属性值,此处没有具体演示代码。
多态
上面代码演示了“类的继承”,这就是鸭子类型,长得像“类”,有属性和方法,并且还对属性实现了封装,就可以称为“类”。如果子结构体没有覆盖父结构的方法,就会默认调用父类的方法。而通过重写覆盖父类方法可以容易的实现多态。
不过这里有个小问题,继承的子结构与父结构不是一个类型了,虽然每个子结构体都实现了对应的方法,如何进行统一处理呢?
要实现多态还需要借助interface
,将上面的Cry
方法统一放在interface
中。
type Action interface {
Cry()
}
上面的Animal、Dog、Duck
结构体都实现了Cry
方法,也是Action类型了。那就可以作为Action来统一处理。
type Cat struct {
Animal
Type string
}
func (cat *Cat) Cry() {
fmt.Println(cat.Name, ", I can cry Miu Miu!")
}
func main() {
dog := Dog{Animal: Animal{Name: "Jason"}, Type: "哺乳动物"}
duck := Duck{Animal: Animal{Name: "Tang"}, Type: "卵生动物"}
cat := Cat{Animal: Animal{Name: "Tom"}, Type: "哺乳动物"}
var animal = []Action{&dog, &duck}
animal = append(animal, &cat)
for _, each := range animal {
each.Cry()
}
}
// Jason ,I can cry Wow Wow!
// Tang ,I can Cry Ga Ga!
// Tom , I can cry Miu Miu!
通过上面的例子,基本上实现了面向对象的一些特性,为什么是基本上呢?原因是面向对象编程在对象创建之后,如果没有构造方法会继承父类的构造方法,而Go语言嵌套结构体实例化仍然需要输入父结构体类型,如果多层嵌套,那么初始化的代码将非常难看。
构造方法
给每个结构体单独写一个构造函数,函数内部完成实例化的动作。比如给Dog
、Duck
、Cat
分别写一个构造函数。这里只写Dog
的构造方法,其余类似。
// 构造方法
func NewDog(name, _type string, age uint8) *Dog {
return &Dog{
Animal: Animal{Name: name, Age: age},
Type: _type,
}
}
dog := NewDog("Jason", "哺乳动物", 10)
duck := NewDuck("Tang", "卵生动物", 2)
cat := NewCat("Tom", "哺乳动物", 3)
通过构造方法直接实例化结构体.
多继承问题
继承是面向对象中非常重要的概念,上面的代码只实现了单继承,通过匿名嵌套结构体就可以了。如果是多继承,也就是嵌套多个结构体,每个结构体无法避免的存在相同的属性名和方法名,这样在访问的时候就会出问题,是的编译器不会让这种情况发生。
比如定义奇怪的Robot,继承Dog
和Cat
,具有两种动物的属性和方法,依然使用匿名嵌套的方式定义:
type Robot struct {
Dog
Cat
}
r := Robot{Dog: *dog, Cat: *cat}
fmt.Println(r.Dog.Age)
fmt.Println(r.Cat.Age)
如果直接访问属性和访问,编译器报错。最好的方式是定义具名嵌套,这样就不会出错了。
type Robot struct {
Dog *Dog
Cat *Cat
}
r := Robot{Dog: dog, Cat: cat}
fmt.Println(r.Dog.Age)
r.Dog.Cry()
看起来使用差不多,实际上代码补全提示时,不会提示非法的属性和方法。
总结
本文主要讨论了Go语言通过鸭子类型来实现面向对象编程,可以看到通过组合几种数据结构,基本能够模拟出面向对象的大部分概念,这正是Go语言设计的哲学,语言的设计尽量简洁,去除多余的概念,通过组合来实现更多数据结构和编程方法。