接口
接口是Go语言整个类型系统的基石。
其他语言的接口
Go语言的接口并不是其他语言(C++、Java、C#等)中所提供的接口概念。
在Go语言出现之前,接口主要作为不同组件之间的契约存在。对契约的实现是强制的,你必须声明你的确实现了该接口。为了实现一个接口,你需要从该接口继承:
interface IFoo { void Bar(); } class Foo implements IFoo { // Java文法 // ... } class Foo : public IFoo { // C++文法 // ... } IFoo* foo = new Foo;
即使另外有一个接口IFoo2实现了与IFoo完全一样的接口方法甚至名字也叫IFoo只不过位于不同的名字空间下,编译器也会认为上面的类Foo只实现了IFoo而没有实现IFoo2接口。
这类接口我们称为侵入式接口。“侵入式”的主要表现在于实现类需要明确声明自己实现了某个接口。
设想我们现在要实现一个简单搜索引擎(SE),它需要依赖两个模块,一个是哈希表(HT),一个是HTML分析器(HtmlParser)。搜索引擎的实现者认为,SE对HT的依赖是确定性的,所以不需要在SE和HT之间定义接口,而是直接通过import(或者include)的方式使用HT;而模块SE对HtmlParser的依赖是不确定的,未来可能需要有WordParser、PdfParser等模块来替代HtmlParser,以达到不同的业务要求。为此,他定义了SE和HtmlParser之间的接口,在模块SE中通过接口调用方式间接引用模HtmlParser。
应当注意到,接口的需求方是SE,只有SE才知道接口应该定义成什么样子,但是接口的实现方是HtmlParser。基于模块设计的单向依赖原则,模块HtmlParser实现自身的业务时,不应该关心某个具体使用方的要求。HtmlParser在实现的时候,甚至还不知道未来有一天SE会用上它。期望模块HtmlParser能够知道需求方需要的所有接口,并提前声明实现这些接口是不合理的。
同样的道理发生在SE自己身上。SE并不能够预计未来会有哪些需求方会用到自己,并且实现它们所要求的接口。
这个问题在设计标准库时变得更加突出,比如我们实现了File类,它有下面这些方法:
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
那么,到底是应该定义一个IFile接口,还是应该定义一系列的IReader、IWriter、
ISeeker、ICloser接口,然后让File从它们继承好呢?脱离了实际的用户场景,讨论这两个
非侵入式接口
在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) func (f *File) Close() 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类并没有从这些接口继承,甚至可以不知道这些接口的存在,但是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继承树图:http://docs.oracle.com/javase/1.4.2/docs/api/overview-tree.html
在Go中,类的继承树并无意义,你只需要知道这个类实现了哪些方法,每个方法是啥含义就足够了。
其二,实现类的时候,只需要关心自己应该提供哪些方法,不用再纠结接口需要拆得多细才合理。接口由使用方按需定义,而不用事前规划。
其三,不用为了实现一个接口而导入一个包,因为多引用一个外部的包,就意味着更多的耦合。接口由使用方按自身需求来定义,使用方无需关心是否有其他模块定义过类似的接口。
接口赋值
接口赋值在Go语言中分为如下两种情况:
将对象实例赋值给接口;
将一个接口赋值给另一个接口。
先讨论将某种类型的对象实例赋值给接口,这要求该对象实例实现了接口要求的所有方法,例如一个Integer类型,如下:
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) }
现在有个问题:假设我们定义一个Integer类型的对象实例,怎么将其赋值给LessAdder接口呢?应该用下面的语句(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接口。而从另一方面来说,根据
func (a *Integer) Add(b Integer)
这个函数无法自动生成以下这个成员方法:
func (a Integer) Add(b Integer) { (&a).Add(b) }
go语言中的接口(二)https://developer.aliyun.com/article/1391461