Go的接口

千寻客 · · 620 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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

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

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

所以正常情况下,如果有如下一个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。然后我们希望在自己的系统中使用它,但是处于代码洁癖的原因,我们不希望依赖我们的代码一个实现,而希望是依赖一个接口。这个时候在go语言中就很简单,我们只需要自己定义一个接口(比如Logger),里面罗列上SomeLogger中提供的我们需要使用的方法即可。这是我们的代码就可以依赖我们自己的Logger接口,来使用SomeLogger提供的功能。

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

相比之下,java是需要显式声明实现的接口的,即如果一个类实现了A接口,那么假设有另一个接口B,即使它的方法列表与A接口完全相同,它也跟实现类没有关系,除非实现类显式声明实现这个接口B。对于上面那种业务不希望依赖实现,而希望依赖与接口的情况,我们一般只能借助于适配器模式[1],如下图所示。

图2:桥接模式

两个结构对比,我们显然能看到java里面则多了很多适配器的包(这有时候也是复杂的一个原因)。自己定义适配器,有时候真的非常复杂。而Golang的这种自动实现方式简洁很多,能减少很多代码量。像这种为一套实现定义接口,而业务依赖于这一套接口,这其实是一种很实用的做法,他能有效的给组件之间解耦。

自动实现的机制还比较死板

虽然自动实现很实用,但是现在Golang的实现机制并不完善。比如对于下面这样一个例子。

// 假设有两个数据类型:接口A和它的一个实现类型B
type A interface {
}

type B struct {
  // implemented A interface
}

// 请注意到:下面的类型D和接口C之间没有实现关系
type C interface {
  Foo() A
}

type D struct {
}

func (D) Foo() B {
  ...
}

这里Golang会认为D类型没有实现C接口,因为D中的Foo方法返回的是B类型,而接口C中要求Foo方法返回的是A类型,所以他们是不一样的。这其实会给我们带来很多不便。比如有一个kv数据库的SDK有如下的几个类:

type SomeClient struct {
  GetConn() SomeConn
}

type SomeConn struct {
  Request(req SomeReq) SomeResp
}

我们还是希望我们的代码不依赖实现,而是依赖接口,这个时候我们可能会定义如下这样的接口KvClientKvConn,希望SomeConn实现KvConn,而SomeClient实现KvClient,这样我们的代码就可以真的依赖我们定义的接口了。但是很遗憾,SomeConn并不实现KvConn!原因就是它的GetConn方法返回的类型KvConnSomeConn不是同一个类型!

type KvClient interface {
  GetConn() KvConn
}

type KvConn interface {
  Request(req KvRequest) KvResponse
}

接口实现关系,Java的处理规则真的值得Golang好好学习!Java的处理规则是里氏替换规则[2]。换句话来说就是:调用接口方法的地方,将接口的方法替换成实现类的方法,接口调用语义依旧适用,那么实现类中的方法就可以覆盖接口的方法。这样说还是有点绕口,用简单的例子来说明吧

interface A {
  Object foo();
}

class B implements A {
  public B foo() {
    return this;
  }
}

比如对于上面的例子,B中的foo方法与A接口中定义的foo方法的返回值虽然不同,但是认为是可以覆盖。因为调用A.foo()的地方期望的是一个Object类型的返回值,而B.foo返回的B类型实例就是Object类型的实例,所以调用的逻辑是完全正确的。

A a = new B();
Object b = a.foo();

其实参数本来也可以使用这种覆盖规则的,但是由于他与重载冲突,所以java中没有允许入参也使用这种替换规则。如果Golang后续能使用这种方式确定是否实现了目标方法,那么前面提到的SomeClient不能实现KvClient的问题就迎刃而解了。

最后,在现在没有使用里氏替换的规则的情况下,SomeClientKvClient的问题应该如何解决?答案是使用适配器来实现,这是一个更通用更具有一般性的解决方案。


  1. 适配器模式

  2. 里氏替换原则


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:千寻客

查看原文:Go的接口

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

620 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传