Go 是静态类型的编程语言,自 2009 年发布以来,已经十多岁了。现在,Go 广泛应用于云原生系统、微服务、web 开发、运维等场景,并且在 webassembly、嵌入式等也有进一步的探索。尤其在中国,Go 语言越来越多的被众多公司和开发者所采用,相关的研究文章也不断的涌现,从编译器的优化、运行时的内部机制、标准库的设计、语言规范的探索、第三库的研究和应用、产品中的应用、不同语言的比较等等目不暇接。
官方 FAQ 给出了标准答案: Yes and No。
当然,Go 有面向对象编程的类型和方法的概念,但是它没有继承(hierarchy)一说。Go 语言的接口实现和其它的编程语言不一样,Go 开发者的初衷就是保证它易于使用,用途更广泛。
还有一种“模拟”产生子类的方法,拿就是通过在类型中嵌入其它的类型,但是这是一种“组合”的方式,而不是继承。
没有了继承, Go 语言的对象变得比 C++和 Java 中更轻量级。
在 Go 语言中,接口定义了一套方法的集合,任何实现这些方法的对象都可以被认为实现了这个接口,这也称作Duck Type。这不像其它语言比如 java,需要预先声明类型实现了某个或者某些接口,这使得 Go 接口和类型变得很轻量级,它解耦了接口和具体实现的硬绑定。显然这是 Go 的开发者深思熟虑的一个决定。
if something looks like a duck, swims like a duck and quacks like a duck then it’s probably a duck.
因为没有继承,你也只能通过 Go 接口实现面向对象编程的多态。本身 Go 接口在内部实现上也是一个(其实是两种,其中一种专门处理interface{})结构体,它的虚函数指向具体的类型的实现。在编译代码的时候,Go 编译器还会做优化,不需要接口的时候,它会使用具体的方法来代替接口使用,这样进一步优化性能,这叫做devirtualize[1]调用。
如果你在一个接口A中嵌入两个接口B、C, 如果B、C有相同的方法的话,编译会出错,但是 Go1.14 修复了这个问题,允许嵌入的接口有重叠。
type A interface { B // 嵌入 C // 嵌入 } type B interface { M() } type C interface { M() }
但是,结构体中嵌入多个接口如果有重叠的方法的话,编译还是会有问题,原因在于结构体在调用重叠方法的时候会迷惑,不知道该调用哪个字段上的方法,所以结构体上的方法不允许有重叠(只允许有覆盖)。
Go 中的接口应用广泛,几乎所有的项目中都会用到接口。但是如何才能用好接口,或者说大家怎么使用结构的呢。本文作者调研了标准库和挑选了知名度最高的几个 Go 语言的应用,分析了他们的使用方法,以供参考。
首先我们会分析接口嵌入的问题,看看在接口中嵌套接口这种使用模式是否流行,并且统计嵌入的接口的数量。
再次,我们定义嵌入的深度。如果没有嵌入,我们定义嵌入深度为0,如果有嵌入,并且嵌入的接口没有嵌入的话,我们称之为深度为1,以此类推。比如下面接口A的嵌入深度为2:
type A interface { B // 嵌入 } type B interface { C } type C interface { }
又比如标准库中的io.ReadCloser它的嵌入深度为1。
嵌入深度严重影响代码的复杂度,所以本文也会分析标准库和流行应用中的接口深度的设计。
第三,我们会分析接口中定义的方法的数量,看看大家使用的接口是一个"巨大"接口还是简单的接口。因为接口中可能还会嵌入接口,我们把方法分成两类,一类是接口中直接定义的方法,我们叫“直接方法”,另外一种是通过嵌入接口中引入的方法,我们称之为"嵌入方法",这两种方法之和就是"全部方法"。
因为标准库是 Go 开发者自己实现的,它的风格代表着“正统的”Go 语言的风格,所以我们在统计分析的时候会单独把它列出来。
另外我们分析了 Docker、etcd、grpc-go、prometheus、consul、influxdb 等几个 Go 开发的应用,统计分析他们对接口的使用情况。
嵌入的接口的数量
比如标准库中的 io package 中的几个接口,Reader和CLoser没有嵌入其它的接口,而ReadCloser嵌入了Reader和CLoser,所以它的嵌入数量为 2。
type Reader interface { Read(p []byte) (n int, err error) } type Closer interface { Close() error } type ReadCloser interface { Reader Closer }
可以看到, 绝大部分的接口(131 个)都不会嵌入其它接口的,嵌入最多的是mime/multipart.File[2],嵌入了四个接口:
type File interface { io.Reader io.ReaderAt io.Seeker io.Closer }
其它 6 个项目使用接口的情况:
可以看到同样大部分接口嵌入数量都在 0 个或者 1 个,嵌入最多的是 kubernetes 的CoreV1Interface[3]接口,嵌入了 16 个接口,可以说是一个巨无霸嵌入接口了:
type CoreV1Interface interface { RESTClient() rest.Interface ComponentStatusesGetter ConfigMapsGetter EndpointsGetter EventsGetter LimitRangesGetter NamespacesGetter NodesGetter PersistentVolumesGetter PersistentVolumeClaimsGetter PodsGetter PodTemplatesGetter ReplicationControllersGetter ResourceQuotasGetter SecretsGetter ServicesGetter ServiceAccountsGetter }
嵌入的接口深度
接口嵌入接口,嵌入的接口再嵌入接口......, 接口的深度可以很长,但是很深的接口降低了代码的可读性,提高了代码的复杂度。让我们看看标准库和精选项目中接口的深度。
标准库中不使用太深的嵌入方式,比较多的也就是嵌入一次。
精选项目中使用接口的方式也一样,很少使用嵌入深度很长的方式,最长也就是 2,而且只有两个接口:kubernetes/.../Stream[4]和moby/.../WriteCommitCloser[5],而且主要是因为嵌入io.ReadWriteCloser和io.WriteCloser导致。
接口中直接定义的方法的数量
接下来让我们看看接口本身的复杂度,也就是直接定义的方法的数量,不包括嵌入接口引入的方法。
标准库中接口定义的直接方法的数量都很少,不会设置很多的方法,接口都比较精巧干练,比较特殊的是reflect/Type[6]接口,定义了 31 个直接方法。
同样,精选项目中的接口直接定义的方法也比较少,超过 10 个方法的接口少之又少,最多的是influxdata/.../TSMFile[7]接口,定义了足足 42 个方法。
接口中总的方法的数量
我们把直接定义的方法和嵌入接口引入的方法加和,统计接口的总方法。
标准库中 1 接口的总方法数量基本都在 8 个以下。
精选项目中接口定义的总方法基本都在 12 个以下。
所以,你知道一般应该定义什么样的接口了吗?
扫码二维码 获取免费视频学习资料
- 本文固定链接: http://phpxs.com/post/7179/
- 转载请注明:转载必须在正文中标注并保留原文链接
- 扫码: 扫上方二维码获取免费视频资料