文章目录
  1. 1. 接口(interface)
    1. 1.1. 其他语言(C++/Java/C#)的接口
    2. 1.2. 非侵入式接口
    3. 1.3. 接口赋值
    4. 1.4. 接口查询
    5. 1.5. 类型查询
    6. 1.6. Any类型

接口(interface)

接口(interface)在Go语言有着至关重要的地位。如果说goroutine和channel 是支撑起Go语言的并发模型的基石,让Go语言在如今集群化与多核化的时代,成为一道极为亮丽的风景;那么接口(interface)是Go语言整个类型系统(type system)的基石,让Go语言在基础编程哲学的探索上,达到史无先例的高度。
Go 语言的接口(interface)不单单只是接口。

为什么这么说?让我们细细道来。

其他语言(C++/Java/C#)的接口

Go语言的接口,并不是你之前在其他语言(C++/Java/C#等)中接触到的接口。
在Go语言之前的接口(interface),主要作为不同组件之间的契约存在。对契约的实现是强制的,你必须声明你的确实现了该接口。为了实现一个接口,你需要从该接口继承:

1
2
3
4
5
6
7
8
9
10
interface IFoo {
void Bar();
}
class Foo implements IFoo { // Java 文法
...
}
class Foo : public IFoo { // C++ 文法
...
}
IFoo* foo = new Foo;

哪怕另外存在一个一模一样的接口,只是名字不同叫IFoo2(名字一样但是在不同的名字空间下,也是名字不同),上面的类Foo只实现了IFoo,但没有实现IFoo2。
这类接口(interface),我们称之为侵入式的接口。“侵入式”的主要表现在于实现类需要明确声明自己实现了某个接口。
这种强制性的接口继承,是面向对象编程(OOP)思想发展过程中的一个重大失误。我之所以这样讲,是因为它从根本上是违背事物的因果关系的。
让我们从契约的形成过程谈起。设想我们现在要实现一个简单搜索引擎(SE)。该搜索引擎需要依赖两个模块,一个是哈希表(HT),一个是HTML分析器(HtmlParser)。
搜索引擎的实现者认为,SE对哈希表(HT)的依赖是确定性的,所以他不并认为需要在SE和HT之间定义接口,而是直接import(或者include)的方式使用了HT;而模块SE对HtmlParser的依赖是不确定的,未来可能需要有WordParser、PdfParser等模块来替代HtmlParser,以达到不同的业务要求。为此,他定义了SE和HtmlParser之间的接口,在模块SE中通过接口调用方式间接引用模块HtmlParser。
应当注意到,接口(interface)的需求方是搜索引擎(SE)。只有SE才知道接口应该定义成什么样子才比更为合理。但是接口的实现方是HtmlParser。基于模块设计的单向依赖原则,模块HtmlParser实现自身的业务时,不应该关心某个具体使用方的要求。HtmlParser在实现的时候,甚至还不知道未来有一天SE会用上它。 要求模块HtmlParser知道所有它的需求方需要的接口,并提前声明实现了这些接口是不合理的。同样的道理发生在搜索引擎(SE)自己身上。SE并不能够预计未来会有哪些需求方需要用到自己,并且实现他们所要求的接口。
这个问题在标准库的提供来说,变得更加突出。比如我们实现了File类(这里我们用Go语言的文法来描述要实现的方法,请忽略文法上的细节),它有这些方法:

1
2
3
4
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从他们继承好呢?脱离了实际的用户场景,讨论这两个设计哪个更好并无意义。问题在于,实现File类的时候,我怎么知道外部会如何用它呢?
正因为这种不合理的设计,使得Java、C# 的类库每个类实现的时候都需要纠结:

  • 问题1:我提供哪些接口好呢?
  • 问题2:如果两个类实现了相同的接口,应该把接口放到哪个包好呢?

    非侵入式接口

    在Go语言中,一个类只需要实现了接口要求的所有函数,那么我们就说这个类实现了该接口。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    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类实现了这些接口,可以进行赋值:

1
2
3
4
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/7/docs/api/overview-tree.html
    在Go中,类的继承树并无意义。你只需要知道这个类实现了哪些方法,每个方法是啥含义就足够了。
  • 其二,实现类的时候,只需要关心自己应该提供哪些方法。不用再纠结接口需要拆得多细才合理。接口是由使用方按需定义,而不用事前规划。
  • 其三,不用为了实现一个接口而import一个包,目的仅仅是引用其中的某个interface的定义,这是不被推荐的。因为多引用一个外部的package,就意味着更多的耦合。接口由使用方按自身需求来定义,使用方无需关心是否有其他模块定义过类似的接口。

    接口赋值

    接口(interface)的赋值在Go语言中分为如下2种情况讨论:
  1. 将对象实例赋值给接口
  2. 将接口赋值给另一个接口
    先讨论将某种类型的对象实例赋值给接口。这要求该对象实例实现了接口要求的所有方法。例如,在之前我们有实现过一个Integer类型,如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    type Integer int

    func (a Integer) Less(b Integer) bool {
    return a < b
    }

    func (a *Integer) Add(b Integer) {
    *a += b
    }

相应地,我们定义接口LessAdder,如下:

1
2
3
4
type LessAdder interface {
Less(b Integer) bool
Add(b Integer)
}

现在有个问题:假设我们定义一个Integer类型的对象实例,怎么其赋值给LessAdder接口呢?应该用下面的语句(1),还是语句(2)呢?

1
2
3
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)
这个函数无法自动生成

1
2
3
func (a Integer) Add(b Integer) {
(&a).Add(b)
}

因为 (&a).Add改变的只是函数参数a,对外部实际要操作的对象并无影响,这不符合用户的预期。故此,Go语言不会自动为其生成该函数。因此,类型Integer只存在Less方法,缺少Add方法,不满足LessAdder接口,故此上面的语句(2)不能赋值。
为了进一步证明以上的推理,我们不妨再定义一个Lesser接口,如下:

1
2
3
type Lesser interface {
Less(b Integer) bool
}

然后我们定义一个Integer类型的对象实例,将其赋值给Lesser接口:

1
2
3
var a Integer = 1
var b1 Lesser = &a ... (1)
var b2 Lesser = a ... (2)

正如如我们所料的那样,语句(1)和语句(2)均可以编译通过。
我们再来讨论另一种情形:将接口赋值给另一个接口。在Go语言中,只要两个接口拥有相同的方法列表(次序不同不要紧),那么他们就是等同的,可以相互赋值。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
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)
}

这里我们定义了两个接口,一个叫 one.ReadWriter,一个叫 two.IStream。两者都定义了Read、Write方法,只是定义的次序相反。one.ReadWriter先定义了Read再定义Write,而two.IStream反之。
在Go语言中,这两个接口实际上并无区别。因为:

  • 任何实现了one.ReadWriter接口的类,均实现了two.IStream。
  • 任何one.ReadWriter接口对象可赋值给two.IStream,反之亦然。
  • 在任何地方使用one.ReadWriter接口,和使用two.IStream并无差异。
    以下这些代码可编译通过:
    1
    2
    3
    var file1 two.IStream = new(File)
    var file2 one.ReadWriter = file1
    var file3 two.IStream = file2

接口赋并不要求两个接口必须等价。如果接口A方法列表是接口B方法列表的子集,那么接口B可以赋值给接口A。例如假设我们有Writer接口:

1
2
3
type Writer interface {
Write(buf []byte) (n int, err error)
}

我们可以将上面的one.ReadWriter、two.IStream接口的实例赋值给Writer接口:

1
2
var file1 two.IStream = new(File)
var file4 Writer = file1

但是反过来并不成立:

1
2
var file1 Writer = new(File)
var file5 two.IStream = file1 // 编译不能通过!

这段代码无法编译通过。原因是显然的:file1并没有Read方法。

接口查询

有办法让上面Writer接口转换为two.IStream接口么?有。那就是我们即将讨论的接口查询语法。代码如下:

1
2
3
4
var file1 Writer = ...
if file5, ok := file1.(two.IStream); ok {
...
}

这个if语句的含义是:file1接口指向的对象实例是否实现了two.IStream接口呢?如果实现了,则… 接口查询是否成功,要在运行期才能够确定。它不像接口赋值,编译器只需要通过静态类型检查即可判断赋值是否可行。
在Windows下做过开发的人,通常都接触过COM,知道COM也有一个接口查询(QueryInterface)。是的,Go语言的接口查询和COM的接口查询(QueryInterface)非常类似,都可以通过对象(组件)的某个接口来查询对象实现的其他接口。当然Go语言的接口查询优雅很多。在Go语言中,对象是否满足某个接口、通过某个接口查询其他接口,这一切都是完全自动完成的。
让语言内置接口查询,这是一件非常了不起的事情。在COM中实现QueryInterface的过程非常繁复,但QueryInterface是COM体系的根本。COM书籍对QueryInterface的介绍,往往从类似下面这样一段问话开始,它在Go语言中同样适用:

> 你会飞吗?    // IFly
> 不会。
> 你会游泳吗?    // ISwim
> 会。
> 你会叫么?    // IShout
> 会。
> ...

随着问题深入,你从开始对对象(组件)一无所知(在Go语言中是interface{},在COM中是IUnknown),到逐步有了深入的了解。
但是你最终能够完全了解对象么?COM说不能,你只能无限逼近,但永远不能完全了解一个组件。Go语言说:你能。
在Go语言中,你可以向接口询问,它指向的对象是否是某个类型,例子如下:

1
2
3
4
var file1 Writer = ...
if file6, ok := file1.(*File); ok {
...
}

这个if语句的含义是:file1接口指向的对象实例是否是 *File 类型呢?如果是的,则…
你可以认为查询接口所指向的对象是否是某个类型,只是接口查询的一个特例。接口是对一组类型的公共特性的抽象。所以查询接口与查询具体类型的区别,好比是下面这两句问话的区别:

> 你是医生吗?
> 是。
> 你是某某某?
> 是。

第一句问话查的是一个群体,是查询接口;而第二句问话已经到了具体的个体,是查询具体类型。
在C++/Java/C# 等语言中,也有一些类似的动态查询能力,比如查询一个对象的类型是否是继承自某个类型(基类查询),或者是否实现了某个接口(接口派生查询)。但是他们的动态查询与Go的动态查询很不一样。

> 你是医生吗?

对于这个问题,基类查询看起来像是在这么问:“你老爸是医生吗?”;接口派生查询则看起来像是这么问:“你有医师执照吗?”;在Go语言中,则是先确定满足什么样的条件才是医生,比如技能要求有哪些,然后才是按条件一一拷问,确认是否满足条件,只要满足了你就是医生,不关心你是否有医师执照,或者是小国执照不被天朝承认。

类型查询

在Go语言中,你还可以更加直接了当地询问接口指向的对象实例的类型。例如:

1
2
3
4
5
6
var v1 interface{} = ...
switch v := v1.(type) {
case int: // 现在v的类型是int
case string: // 现在v的类型是string
...
}

就像现实生活中物种多得数不清一样,语言中的类型也多的数不清。所以类型查询并不经常被使用。它更多看起来是个补充,需要配合接口查询使用。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Stringer interface {
String() string
}
func Println(args ...interface{}) {
for _, arg := range args {
switch v := arg.(type) {
case int: // 现在v的类型是int
case string: // 现在v的类型是string
default:
if v, ok := arg.(Stringer); ok { // 现在v的类型是Stringer
val := v.String()
...
} else {
...
}
}
}

Go语言标准库的Println当然比这个例子要复杂很多。我们这里摘取其中的关键部分进行分析。对于内置类型,Println采用穷举法来,针对每个类型分别转换为字符串进行打印。对于更一般的情况,首先确定该类型是否实现了String()方法,如果实现了则用String()方法转换为字符串进行打印(自定义的结构体可以实现一个String()函数来限定打印格式)。否则,Println利用反射(reflect)遍历对象的所有成员变量进行打印。
是的,利用反射(reflect)也可以进行类型查询,详细可参阅reflect.TypeOf方法相关文档。。

Any类型

由于Go语言中任何对象实例都满足空接口interface{},故此interface{}看起来像是可以指向任何对象的Any类型。如下:

1
2
3
4
5
var v1 interface{} = 1      // 将int类型赋值给interface{}
var v2 interface{} = "abc" // 将string类型赋值给interface{}
var v3 interface{} = &v2 // 将*interface{}类型赋值给interface{}
var v4 interface{} = struct{ X int }{1}
var v5 interface{} = &struct{ X int }{1}

当一个函数可以接受任意的对象实例时,我们会将其声明为interface{}。最典型的例子是标准库fmt中PrintXXX系列的函数。例如:

1
2
func Printf(fmt string, args ...interface{})
func Println(args ...interface{})

前面我们已经简单分析过Println的实现,也已经展示过interface{}的用法。总结来说,interface{} 类似于COM中的IUnknown,我们刚开始对其一无所知,但我们可以通过接口查询和类型查询逐步了解它。
总结
我们说,Go 语言的接口(interface)不单单只是接口。在其他语言中,接口仅仅作为组件间的契约存在。从这个层面讲,Go语言接口的重要突破是,其接口是非侵入式的,把其他语言接口的副作用消除了。
但是Go语言的接口不仅仅是契约作用。它是Go语言类型系统(type system)的纲。这表现在:

  • 接口查询:通过接口你可以查询接口所指向的对象是否实现了另外的接口。
  • 类型查询:通过接口你可以查询接口所指向的对象的具体类型。
  • Any类型:在Go语言中interface{}可指向任意的对象实例。
文章目录
  1. 1. 接口(interface)
    1. 1.1. 其他语言(C++/Java/C#)的接口
    2. 1.2. 非侵入式接口
    3. 1.3. 接口赋值
    4. 1.4. 接口查询
    5. 1.5. 类型查询
    6. 1.6. Any类型