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!")
}

封装通过SetGet方法提供属性的修改和访问,方便保护属性的安全。把属性定义小写字母开头,不支持导出即可,这样在别的包使用时,只能通过SetGet方法,而不能直接访问属性值,此处没有具体演示代码。

多态

上面代码演示了“类的继承”,这就是鸭子类型,长得像“类”,有属性和方法,并且还对属性实现了封装,就可以称为“类”。如果子结构体没有覆盖父结构的方法,就会默认调用父类的方法。而通过重写覆盖父类方法可以容易的实现多态。

不过这里有个小问题,继承的子结构与父结构不是一个类型了,虽然每个子结构体都实现了对应的方法,如何进行统一处理呢?

要实现多态还需要借助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语言嵌套结构体实例化仍然需要输入父结构体类型,如果多层嵌套,那么初始化的代码将非常难看。

构造方法

给每个结构体单独写一个构造函数,函数内部完成实例化的动作。比如给DogDuckCat 分别写一个构造函数。这里只写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,继承DogCat,具有两种动物的属性和方法,依然使用匿名嵌套的方式定义:

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语言设计的哲学,语言的设计尽量简洁,去除多余的概念,通过组合来实现更多数据结构和编程方法。


  1. https://zh.wikipedia.org/wiki/鸭子类型 ↩︎