美文网首页
Golang的接口

Golang的接口

作者: 千寻客 | 来源:发表于2019-07-10 00:56 被阅读0次

有意思的接口规则:自动实现

Golang也支持接口,但是它的接口规则很有意思:

一个类型不需要显示声明它要实现的接口,只要实现了接口中规定的所有方法,那么就自动实现了相应的接口了。[1]

所以正常情况下,如果有如下一个struct

type A struct {
}

func (*A) func Foo() {}
func (*A) func Bar(i int) int {
  return i
}

那么它就自动同时实现了下面三个接口

type IFoo interface {
  Foo()
}

type IBar interface {
  Bar(i int) int
}

type IFooBar interface {
  IFoo
  IBar
}

所以我们可以进行如下的赋值:

var a *A = &A{}

// 下面三个赋值都是正确的
var foo IFoo = a
var bar IBar = a
var foobar IFooBar = a

这种自动的接口实现带来了一个很大的好处:功能的使用方可以自由的给功能实现定义自己喜欢的接口,并且通过自己定义的接口使用该功能。这在某些时候是很有用的。

自动实现带来的便利

比如某个很实用的功能(比如日志打印功能)有第三方一个实现(假设是SomeLogger),我们希望在自己的系统中使用它。但是出于代码整洁的考虑,我们不希望代码直接依赖第三方实现,而希望是依赖一个接口:这样当需要更换实现的时候就不用满世界改代码。

这个问题Golang的处理很简单:我们只需要自己定义一个接口(比如Logger),里面罗列上SomeLogger中提供的并且我们需要使用的方法即可。这时我们就可以依赖我们自己的Logger接口来使用SomeLogger提供的功能。

图1:给第三方实现定义接口

相比之下,Java是需要显式声明实现的接口的,比如图2中类SomeLogger实现了ILogger接口,即使ILogger接口与业务自己定义的Logger接口的方法列表完全相同,SomeLogger类与Logger接口也没有“实现”的关系。对于前面提到的那种业务不希望依赖实现,而希望依赖与接口Logger的情况,我们一般只能借助于适配器模式[2]来实现,如图2bridge部分所示。

图2:桥接模式

两个结构对比,我们显然能看到java里面则多了很多代码。自己定义适配器,有时候是非常复杂的,而且还容易引发各种依赖的问题。Golang的这种自动实现方式则很简洁而且灵活很多。像这种为一套实现定义接口,而业务依赖于这一套接口,这其实是一种很实用的做法,他能有效的给组件之间解耦[3],我们会经常使用,满世界的适配器还是非常恐怖的。

自动实现的问题

虽然自动实现理论上能规避java里面的满世界适配器的局面,但是实际上这很难发生,有些时候还是免不了适配器。这是因为适配器适配的双方一般只是在同一个领域而已,他们在开发过程中不会考虑是否要跟对方使用相同的接口。所以这种情况下,不一定能找到一个接口是两者刚好都能实现。这种时候,相比起来大家蹩脚的实现同一个接口,不如各自自由发展,然后定义适配器来得简单。

其实现在Golang的实现机制并不完善。接口自动实现有一个很大的不足,他不能适用里氏替换原则,这让接口实现很多时候无法摆脱实现类型对接口的强依赖,这对于实现来说是很不友好的。这也会使得对于一些比较复杂的场景,使用方很难定义一个好用的接口。比如看下面这样一个KV存储的客户端的例子:

某种KV存储有一个第三方实现的SDK,提供了客户端类型SomeClientSomeClient提供了一个GetConn()的方法返回该SDK中定义的连接类型SomeConn的实例,SomeConn实例用于真正请求发送。由于业务需要依赖接口,不希望依赖实现,所以我们定义了一套接口KvClientKvConn,希望KvClient能作为SomeClient的接口抽象,KvConn能作为SomeConn的抽象,自然KvClientGetConn()方法返回的是KvConn类型的实例。

图3:自动实现失效的情况

但是此时我们就会惊奇的发现,SomeClient并不能自动实现KvClient接口了。原因是这两个类型的GetConn()方法的返回值类型不一致,所以不认为SomeClient实现了KvClient中的GetConn()方法。但是其实我们是希望这种时候能判定为"实现"关系的,因为实现类中返回的SomeConn类型的实例就是KvConn类型的实例。

接口实现关系,Java的处理规则值得赞赏!Java是使用里氏替换原则[4]来处理方法的覆盖关系的(Override)。比如对于上面的KvClient的例子,Java则认为SomeClientGetConn()方法能覆盖KvClient接口中的GetConn()方法。因为按照KvClient.GetConn(): KvConn的语义调用SomeClientGetConn()方法返回的SomeConn实例都是KvConn类型的实例;所以代码任何使用KvClient实例的地方替换成SomeClient实例都不会出问题。

如果Golang的接口实现规则未来还会变更,希望他能借鉴这种规则。

其实Java的方法覆盖关系也没有完全参照里氏替换原则[4],他对参数里面的类型就没有按照里氏替换的方式处理。

比如接口方法是void foo(Foo),而实现类的方法是void foo(Object),可以肯定能传给接口方法的Foo类型的参数都是可以传给实现类的方法的(接受Object类型)。但是Java没有把这当成合法的覆盖,这主要是因为与重载(Overloading)的规则冲突了。

但是Golang本身就没有方法重载的设计[5],所以它是有条件对里氏替换原则方式作完整支持的。

最后,在现在没有使用里氏替换的规则的情况下,SomeClientKvClient的问题应该如何解决?答案是使用适配器[2]来实现,这是一个古老而又实用的解决方案!

图4:Golang使用适配器

最后,自动实现还有一个致命性的缺点:容易造成接口混乱。golang中,接口一般会倾向于定义的比较简单,从而可以简单灵活的使用各个功能实现代码的各个侧面。但是接口越简单,其他无关的类型就越容易实现他。这可能会造成我们看到满世界都是实现类型,分不清谁是真的是谁是假的,而不得不承受辨别真假美猴王的纠结。这种情况,会给代码的理解和使用带来很多干扰信息。


  1. https://gobyexample.com/interfaceshttps://tour.golang.org/methods/10

  2. 适配器模式

  3. 依赖倒置原则

  4. 里氏替换原则

  5. Golang/ISSUE-21659

相关文章

  • 19年第34周:Go语言 有趣的接口

    一、Golang的接口 Go语言接口设计很符合设计原则参考图灵丛书中的《设计模式》 当我看到Golang的接口时,...

  • Go 学习笔记 11 | Golang 接口详解

    一、Golang 接口 Golang 中接口定义了对象的行为规范,只定义规范不实现。接口中定义的规范由具体的对象来...

  • golang分层测试之http接口测试入门

    前言 本节主要讲使用golang进行接口测试,其中主要以http协议的接口测试来讲 golang中的http请求 ...

  • 接口 interface golang

    原文链接:接口 interface-GOLANG

  • Golang的接口

    有意思的接口规则:自动实现 Golang也支持接口,但是它的接口规则很有意思: 一个类型不需要显示声明它要实现的接...

  • Golang:接口

    什么是接口 在 Golang 中,一个接口是一组方法签名。当一个类型定义了接口里所有定义的方法时,就说这个类型实现...

  • Golang——接口

    接口(interface)定义一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。在go语言中,接...

  • golang接口

    定义 在 Go 中,关键字 interface 被赋予了多种不同的含义。每个类型都有接口,意味着对那个类型定义了方...

  • Golang 接口

  • golang中interface底层分析

    golang中的接口分为带方法的接口和空接口。带方法的接口在底层用iface表示,空接口的底层则是eface表示。...

网友评论

      本文标题:Golang的接口

      本文链接:https://www.haomeiwen.com/subject/jxsqkctx.html