Go语言初探(三):面向接口

Go语言初探

(三)

面向接口

在讲Go语言“面向接口”之前,还是要简单讲一下Java、C++中多态的概念,包括接口、虚函数这类跨语言的基本概念。接着,会讲一下Go语言面向接口的基本内容与好处,最后,比较一下它们在编程上的感觉~由于上一阵子我在做基于Python与基于Go的爬虫,因此将使用Go的爬虫小代码简单解释Go的面向接口。

我总是希望把一个问题讲清楚,尽管它可能在认识上是有偏差的。因此,如果讲得不对或者认识不够深入,请评论告诉我哦。//C++是一生之敌

开 • 门 • 见 • 山

Java、Python等面向对象语言强调对象的概念,都是由被调用者告诉“大家”自己实现了哪些接口,意思就是被调用者主动告知外界“你们可以由我实现的xxx接口调用我”。但Go是由使用者定义接口,意思就是告知被调用者“我需要你实现哪些函数才可以被我调用”。这样的好处主要在于能被调用的类对象只要实现被要求的函数就可以了,因此可以实现更少的接口,避免结构上的臃肿。

Java、C++的多态

依然是直接上代码来说明问题~

这个例子大家应该是无比熟悉了,由于A的FUN方法声明为虚函数,因此无论指针p是A类型的指针还是B类型的指针,都会去调用调用B类型中的方法。C++对虚函数的调用是通过维护一张虚函数表来实现动态调用的,因此在运行过程中可以检索到B类的方法。如果A的FUN方法没有加上virtual关键字,那么通过指针类型为A的指针访问b,调用的将是A类中定义的FUN方法。

Java代码中我们发现没有virtual关键字,但上面的代码运行结果和C++代码相同,这说明Java中Class的方法默认是virtual的。Java不管你前面给变量声明了什么类型,它在调用函数的时候,只参考调用者new出来的类型是什么。

“虚”的概念是比较容易理解的,下面要说一下“抽象”的概念。很多时候,不同的Class都需要去实现一个同名函数,但它们的具体实现方法却是大相径庭的,就比如equals、hashCode这种函数就需要父类仅仅定义要实现这些函数就可以了,由子类自己根据需要来实现它们。而父类往往也会给出一些“孩子”们都共享的函数,有add、put等,这些函数就由父类实现就好了。

也就是说,抽象类是用来捕捉子类的通用特性的 。它不能被实例化,只能被用作子类的超类。抽象类是被用来创建继承层级里子类的模板。

而接口是抽象方法的集合。如果一个类实现了某个接口,那么它就继承了这个接口的抽象方法。

看一下Java集合中List接口的继承关系~其中箭头表示extend关系

List

点击放大观看~

上面这张图给出了继承Iterable接口的所有子接口以及实现了Iterable接口的所有class,可以看到Java集合中的所有class最终都实现了Iterable。Iterable接口中包含forEach和iterator的抽象函数,因此实现它的类都需要实现iterator函数。

所以,实现了Iterable接口的类(A,例如比较常用的ArrayList、HashSet、LinkedList)的实例(a)都可以用以下语句得到其中所有内容~

for(E e : a){

//do something...}

由于Java不支持多继承,也就是一个类不能够继承多个类,但可以实现多个接口,所以说你就可以使用接口来解决这个问题。

题外话~具体应该怎么实现Iterable呢!可以参考下面这张图里的代码。

C++ Java

虚函数

(virtual,包含实现)

普通成员函数

void func();

纯虚函数

(virtual void func() =0,仅定义,无实现)

抽象函数

abstract void func();

抽象类

(需要包括纯虚函数,不能被实例化)

abstract类

abstract class

虚基类

(由纯虚函数构成的基类,完全抽象,不存在方法的实现)

接口

interface

Go的面向接口

上一期我们就讲到了,Go是不支持继承的,只支持封装。简单回顾一下,也就是定义一个struct,这个struct只负责保存变量,而struct的方法则都定义在struct外部。我们调用这些方法的时候,也使用Struct.method(var)的形式,这样,struct的值(或者指针)就会传进函数,从而等同于调用了这个struct的方法。因此,本质上struct的方法和一般的函数调用没什么区别。而我们往往通过把别人写好的struct打包成一个更大的struct,并自己定义一些别的struct的函数,从而可以对这个struct进行扩展。这种“打包”的做法其实跟Java的继承还是很类似的。

既然“打包”的做法比较类似,那么“接口”自然也是十分重要的概念。看一下下面的接口定义~

我们定义了Retriever接口,并规定必须实现Get方法。注意到下面的download函数接收一个实现了Retriever接口的对象,并且返回它的Get函数调用结果。

我们在下面定义一个struct作为接口的实现~注意啦!在这里我们根本没有出现Retriever这个词~不过由于实现了Get,并且接受的参数也是上面接口规定的url类型,因此它仍然被“隐晦”地实现了Retriever接口。

接下来,我们写main函数来调用上面的download,因为我们上面的struct已经实现了Get,所以它是一个合格的接口实现

输出结果如下~可以想见它是将imooc这个网站html里的head标签下面的内容打印下来~

如果一个函数需要调用参数的两种门类的不同属性,比如下面例子的Get、Post,那么就需要用一个更大的接口将这两种(或多种)门类对应的接口“包”起来,并将这个结构上更大的结构的实现类对象作为这个函数的参数。在下面的例子中,我们不仅可以照常Get到网站发回的内容,还可以先进行Post(例如先向目标网站发送一个请求),这样我们Get函数处理的内容就是动态的啦~

说到Go的爬虫实战,笔者用Go简单编写了一下并发爬虫,虽然也会遇到ip地址被封的情况,但爬取速度却比之前python的方法快了很多,其中就用到了之前介绍的goroutine的概念。个人认为,goroutine对于并发编程的便捷是我喜欢Go的最大原因。当然,Go也有一些缺点的,接下来如果时间允许,在(四)中将会带来Go的一些“别扭之处”以及爬虫实战的代码小结~不过在此之前,可能先会推送一些别的内容~敬请期待啦~

谢谢阅读~~

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180822G1337R00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券