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 的问题应该如何解决?答案是使用 适配器 来实现,这是一个更通用更具有一般性的解决方案。