两分钟让你明白Go中如何继承

最近在重构代码的时候,抽象了大量的接口。也使用这些抽象的接口作了不少伪继承的操做,极大的减小了代码冗余,同时也增长了代码的可读性。java

而后随便搜了一下关于Go继承的文章,发现有的文章的代码量过多,而且代码format极其粗糙,命名极其随意,相似于A、B这种,让人看着看着就忘了究竟是谁继承谁,我又要回去看一遍逻辑。web

虽然只是样例代码,我认为仍然须要作到简洁、清晰以及明了。这也是我为何要写这篇博客的缘由。接下里在这里简单分享一下在Go中如何实现继承。服务器

1. 简单的组合

说到继承咱们都知道,在Go中没有extends关键字,也就意味着Go并无原生级别的继承支持。这也是为何我在文章开头用了伪继承这个词。本质上,Go使用interface实现的功能叫组合,Go是使用组合来实现的继承,说的更精确一点,是使用组合来代替的继承,举个很简单的例子。微信

1.1 实现父类

咱们用很容易理解的动物-来举例子,废话很少说,直接看代码。app

type Animal struct {
	Name string
}

func (a *Animal) Eat() {
	fmt.Printf("%v is eating", a.Name)
	fmt.Println()
}

type Cat struct {
	*Animal
}

cat := &Cat{
	Animal: &Animal{
		Name: "cat",
	},
}
cat.Eat() // cat is eating
复制代码

1.2 代码分析

首先,咱们实现了一个Animal的结构体,表明动物类。并声明了Name字段,用于描述动物的名字。frontend

而后,实现了一个以Animal为receiver的Eat方法,来描述动物进食的行为。函数

最后,声明了一个Cat结构体,组合了Cat字段。再实例化一个猫,调用Eat方法,能够看到会正常的输出。微服务

能够看到,Cat结构体自己没有Name字段,也没有去实现Eat方法。惟一有的就是组合了Animal父类,至此,咱们就证实了已经经过组合实现了继承。性能

2. 优雅的组合

熟悉Go的人看到上面的代码可能会发出以下感叹学习

这也太粗糙了吧 -- By 鲁迅:我没说过这句话

的确,上面的仅仅是为了给尚未了解过Go组合的人看的。做为一个简单的例子来理解Go的组合继承,这是彻底没有问题的 。但若是要运用在真正的开发中,那仍是远远不够的。

举个例子,我若是是这个抽象类的使用者,我拿到animal类不能一目了然的知道这个类干了什么,有哪些方法能够调用。以及,没有统一的初始化方式,这意味着凡是涉及到初始化的地方都会有重复代码。若是后期有初始化相关的修改,那么只有一个一个挨着改。因此接下来,咱们对上述的代码作一些优化。

2.1 抽象接口

接口用于描述某个类的行为。例如,咱们即将要抽象的动物接口就会描述做为一个动物,具备哪些行为。常识告诉咱们,动物能够进食(Eat),能够发出声音(bark),能够移动(move)等等。这里有一个颇有意思的类比。

接口就像是一个招牌,好比一家星巴克。星巴克就是一个招牌(接口)。

你看到这个招牌会想到什么?美式?星冰乐?抹茶拿铁?又或者是拿铁,甚至是店内的装修风格。

这就是一个好的接口应该达到的效果,一样这也是为何咱们须要抽象接口。

// 模拟动物行为的接口
type IAnimal interface {
	Eat() // 描述吃的行为
}

// 动物 全部动物的父类
type Animal struct {
	Name string
}

// 动物去实现IAnimal中描述的吃的接口
func (a *Animal) Eat() {
	fmt.Printf("%v is eating\n", a.Name)
}

// 动物的构造函数
func newAnimal(name string) *Animal {
	return &Animal{
		Name: name,
	}
}

// 猫的结构体 组合了animal
type Cat struct {
	*Animal
}

// 实现猫的构造函数 初始化animal结构体
func newCat(name string) *Cat {
	return &Cat{
		Animal: newAnimal(name),
	}
}

cat := newCat("cat")
cat.Eat() // cat is eating
复制代码

在Go中其实没有关于构造函数的定义。例如咱们在Java中可使用构造函数来初始化变量,举个很简单的例子,Integer num = new Integer(1)。而在Go中就须要使用者本身经过结构体的初始化来模拟构造函数的实现。

而后在这里咱们实现子类Cat,使用组合的方式代替继承,来调用Animal中的方法。运行以后咱们能够看到,Cat结构体中并无Name字段,也没有实现Eat方法,可是仍然能够正常运行。这证实咱们已经经过组合的方式了实现了继承。

2.2 重载方法

// 猫结构体IAnimal的Eat方法
func (cat *Cat) Eat() {
	fmt.Printf("children %v is eating\n", cat.Name)
}

cat.Eat()
// children cat is eating
复制代码

能够看到,Cat结构体已经重载了Animal中的Eat方法,这样就实现了重载。

2.3 参数多态

什么意思呢?举个例子,咱们要如何在Java中解决函数的参数多态问题?熟悉Java的可能会想到一种解决方案,那就是通配符。用一句话归纳,使用了通配符可使该函数接收某个类的全部父类型或者某个类的全部子类型。可是我我的认为对于不熟悉Java的人来讲,可读性不是特别友好。

而在Go中,就十分方便了。

func check(animal IAnimal) {
	animal.Eat()
}
复制代码

在这个函数中就能够处理全部组合了Animal的单位类型,对应到Java中就是上界通配符,即一个能够处理任何特定类型以及是该特定类型的派生类的通配符,再换句人话,啥动物都能处理。

3. 总结

凡事都有两面性,作优化也不例外。大量的抽象接口的确能够精简代码,让代码看起来十分优雅、舒服。可是一样,这会给其余不熟悉的人review代码形成理解成本。想象你看某段代码,全是接口,点了好几层才能看到实现。更有的,往下找着找着忽然就在另外一个接口处断掉了,必需要手动的去另外一个注册的地方去找。

这就是我认为优化的时候要面临的几个问题:

  • 优雅
  • 可读
  • 性能

有的时候咱们很难作到三个方面都兼顾,例如这样写代码看起来很难受,可是性能要比优雅的代码好。再例如,这样写看起来很优雅,可是可读性不好等等。

仍是引用我以前博客中常常写的一句话

适合本身的才是最好的

这种时候只能根据本身项目的特定状况,选择最适合你的解决方案。没有万能的解决方案。

分享一句最近弹吉他看到的毒鸡汤,学习也是同样的。

练琴的路上没有捷径,全是弯路

往期文章:

相关:

  • 微信公众号: SH的全栈笔记(或直接在添加公众号界面搜索微信号LunhaoHu)
相关文章
相关标签/搜索