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
。对于上面那种业务不希望依赖实现,而希望依赖与接口的情况,我们一般只能借助于适配器模式,如下图所示。
图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 }
我们还是希望我们的代码不依赖实现,而是依赖接口,这个时候我们可能会定义如下这样的接口 KvClient
和 KvConn
,希望 SomeConn
实现 KvConn
,而 SomeClient
实现 KvClient
,这样我们的代码就可以真的依赖我们定义的接口了。但是很遗憾, SomeConn
并不实现 KvConn
!原因就是它的 GetConn
方法返回的类型 KvConn
与 SomeConn
不是同一个类型!
type KvClient interface { GetConn() KvConn } type KvConn interface { Request(req KvRequest) KvResponse }
接口实现关系,Java的处理规则真的值得Golang好好学习!Java的处理规则是里氏替换规则。换句话来说就是:调用接口方法的地方,将接口的方法替换成实现类的方法,接口调用语义依旧适用,那么实现类中的方法就可以覆盖接口的方法。这样说还是有点绕口,用简单的例子来说明吧
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
的问题就迎刃而解了。
最后,在现在没有使用 里氏替换
的规则的情况下, SomeClient
与 KvClient
的问题应该如何解决?答案是使用 适配器
来实现,这是一个更通用更具有一般性的解决方案。