接口-1
接口在Go语言中有至关重要的作业,接口是Go语言整个类型系统的基石,接口让Go语言在基础编程哲学上达到前所未有的高度,Go语言在编程类型中是改革派,而不是改良派,总而言之,Go语言因为有了接口而更加趋于完美。
其他语言的接口
Go语言的接口和其他语言比如java、c、c++当中的接口不是一个概念。 在Go语言出现之前,接口主要作为不同组件之前的契约出现,自我理解java中的接口是一种能力约定,定义:代表了某种能力。实现方法:能力的具体实现。对契约的实现是强制的,你必须声明你的确实现了该接口。为了实现一个接口,你需要从该接口继承需要实现接口定义的所有方法。如下:
interface IFoo {
void Bar();
}
class Foo implements IFoo { // Java文法
// ...
}
class Foo : public IFoo { // C++文法
// ...
}
IFoo foo = new Foo;
如上文,如果有一个接口也叫IFoo1,接口里面的方法和上文的IFoo一样,只是在不同的命名空间下面,那么Foo实现的也只是我上文的IFoo接口,而和IFoo1接口没有任何关系,这类接口我们称为浸入式接口,“侵入式”主要表现为实现接口的时候需要明确声明自己实现的哪一个接口,这种强制性的接口继承是面向对象编程思想发展中一个遭受很多质疑的特性。
正是因为这种设计其他语言在写接口的时候需要考虑两个方面的的问题:
- 问题一:我提供哪些接口好呢?
- 问题二:如果两个接口实现了相同的的接口,我应该把接口放在哪个包里面好呢? 基于浸入式接口,Go语言出现了非浸入式接口。
非侵入式接口
在Go语言中,一个类只要实现了接口中所要求的所有函数,我们就说这个类实现了该接口。如下:
type File struct {
// ...
}
func (f *File) Read(buf []byte) (n int, err error)
func (f *File) Write(buf []byte) (n int, err error)
func (f *File) Seek(off int64, whence int) (pos int64, err error)
这里我们定义了一个File类,并实现有Read()、Write()、Seek()、Close()等方法。假设我们有如下接口:
type IFile interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
Seek(off int64, whence int) (pos int64, err error)
Close() error
}
type IReader interface {
Read(buf []byte) (n int, err error)
}
type IWriter interface {
Write(buf []byte) (n int, err error)
}
type ICloser interface {
Close() error
}
File和IFile看上去完全没有任何的继承和实现关系,甚至File类都不知道IFile接口的存在,但是File类实现了这些接口,就可以进行赋值:
var file1 IFile = new(File)
var file2 IReader = new(File)
var file3 IWriter = new(File)
var file4 ICloser = new(File)
Go语言这种非侵入式的接口的好处如下:
- 其一:Go语言的标准库,再也不需要绘制类库的继承树图。比如C++、Java、C# 类库的继承树图,这里举例java的继承图。在Go语言中继承图是没有任何意义的,你只需要知道这个类实现了哪些方法,每个方法是啥含义就好,但是自我感觉这种非侵入式,正因为没有继承和实现,我如果不知道这个类实现了哪些接口的方法怎么办,怎么使用?
- 其二:实现类的时候,只需要关心自己应该提供哪些方法,不用再纠结接口需要拆得多细才合理。接口由使用者按需定义,而不用事前规划。
- 其三:不用为了实现一个接口而导入一个包,因为多引用一个外部的包,就意味着更多的耦合。接口由使用方按自身需求来定义,使用方无需关心是否有其他模块定义过类似的接口。
接口赋值
接口赋值在Go语言中可以分为两种情况如下:
- 将对象实例赋值给接口
- 将一个接口赋值给另一个接口 将某种类型的对象实例赋值给接口,这要求该对象实例实现了接口要求的所有方法:如下:
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
func (a *Integer) Add(b Integer) {
*a += b
}
相应地,我们定义接口LessAdder,如下:
type LessAdder interface {
Less(b Integer) bool
Add(b Integer)
}
对象实例赋值,我们应该用下文的语句1还是语句2赋值呢?
var a Integer = 1
var b LessAdder = &a ... (1)
var b LessAdder = a ... (2)
答案是应该用语句(1)。原因在于,Go语言可以根据下面的函数:
func (a Integer) Less(b Integer) bool
自动生成一个新的Less()方法:
func (a *Integer) Less(b Integer) bool {
return (*a).Less(b)
}
这样,类型*Integer就既存在Less()方法,也存在Add()方法,满足LessAdder接口。对象赋值先到这里下文讨论接口对接口赋值。
将一个接口赋值给另一个接口。在Go语言中,只要两个接口拥有相同的方法列表(次序不同不要紧),那么它们就是等同的,可以相互赋值。
第一个接口
package one
type ReadWriter interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
}
第二个接口
package two
type IStream interface {
Write(buf []byte) (n int, err error)
Read(buf []byte) (n int, err error)
}
上文这两接口除过接口名字不同,定义的方法都是Read和Write方法只是顺序相反,在Go语言中这两接口并无任何区别。因为:
- 任何实现了one.ReadWriter接口的类,均实现了two.IStream
- 任何one.ReadWriter接口对象可赋值给two.IStream,反之亦然
- 在任何地方使用one.ReadWriter接口与使用two.IStream并无差异 如下代码都可以编译通过:
var file1 two.IStream = new(File)
var file2 one.ReadWriter = file1
var file3 two.IStream = file2
接口赋值并不要求两个接口必须等价。如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A,但是反过来接口A不能赋值给接口B。因为接口A的方法不全,没法给接口B当中接口A没有的方法赋值。
备注
本文正在参与「掘金Golang主题学习月」, 点击查看活动详情。