法,满足LessAdder接口。而从另一方面来说,根据
这个函数无法自动生成
因为 (&a).Add改变的只是函数参数a,对外部实际要操作的对象并无影响,这不符合用户的预期。故此,Go语言不会自动为其生成该函数。因此,类型Integer只存在Less方法,缺少Add方法,不满足LessAdder接口,故此上面的语句(2)不能赋值。
为了进一步证明以上的推理,我们不妨再定义一个Lesser接口,如下:
然后我们定义一个Integer类型的对象实例,将其赋值给Lesser接口:
正如如我们所料的那样,语句(1)和语句(2)均可以编译通过。
我们再来讨论另一种情形:将接口赋值给另一个接口。在Go语言中,只要两个接口拥有相同的方法列表(次序不同不要紧),那么他们就是等同的,可以相互赋值。例如:
这里我们定义了两个接口,一个叫 one.ReadWriter,一个叫 two.IStream。两者都定义了Read、Write方法,只是定义的次序相反。one.ReadWriter先定义了Read再定义Write,而two.IStream反之。
在Go语言中,这两个接口实际上并无区别。因为:
以下这些代码可编译通过:
接口赋并不要求两个接口必须等价。如果接口A方法列表是接口B方法列表的子集,那么接口B可以赋值给接口A。例如假设我们有Writer接口:
我们可以将上面的one.ReadWriter、two.IStream接口的实例赋值给Writer接口:
但是反过来并不成立:
这段代码无法编译通过。原因是显然的:file1并没有Read方法。
有办法让上面Writer接口转换为two.IStream接口么?有。那就是我们即将讨论的接口查询语法。代码如下:
这个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语言中,你可以向接口询问,它指向的对象是否是某个类型,例子如下:
这个if语句的含义是:file1接口指向的对象实例是否是 *File 类型呢?如果是的,则...
你可以认为查询接口所指向的对象是否是某个类型,只是接口查询的一个特例。接口是对一组类型的公共特性的抽象。所以查询接口与查询具体类型的区别,好比是下面这两句问话的区别:
> 你是医生吗?
> 是。
> 你是某某某?
> 是。
第一句问话查的是一个群体,是查询接口;而第二句问话已经到了具体的个体,是查询具体类型。
在C++/Java/C# 等语言中,也有一些类似的动态查询能力,比如查询一个对象的类型是否是继承自某个类型(基类查询),或者是否实现了某个接口(接口派生查询)。但是他们的动态查询与Go的动态查询很不一样。
> 你是医生吗?
对于这个问题,基类查询看起来像是在这么问:“你老爸是医生吗?”;接口派生查询则看起来像是这么问:“你有医师执照吗?”;在Go语言中,则是先确定满足什么样的条件才是医生,比如技能要求有哪些,然后才是按条件一一拷问,确认是否满足条件,只要满足了你就是医生,不关心你是否有医师执照,或者是小国执照不被天朝承认。
在Go语言中,你还可以更加直接了当地询问接口指向的对象实例的类型。例如:
就像现实生活中物种多得数不清一样,语言中的类型也多的数不清。所以类型查询并不经常被使用。它更多看起来是个补充,需要配合接口查询使用。例如:
Go语言标准库的Println当然比这个例子要复杂很多。我们这里摘取其中的关键部分进行分析。对于内置类型,Println采用穷举法来,针对每个类型分别转换为字符串进行打印。对于更一般的情况,首先确定该类型是否实现了String()方法,如果实现了则用String()方法转换为字符串进行打印。否则,Println利用反射(reflect)遍历对象的所有成员变量进行打印。
是的,利用反射(reflect)也可以进行类型查询,详细可参阅reflect.TypeOf方法相关文档。在后文高阶话题中我们也会探讨有关“反射(reflect)”的话题。
由于Go语言中任何对象实例都满足空接口interface{},故此interface{}看起来像是可以指向任何对象的Any类型。如下:
当一个函数可以接受任意的对象实例时,我们会将其声明为interface{}。最典型的例子是标准库fmt中PrintXXX系列的函数。例如:
前面我们已经简单分析过Println的实现,也已经展示过interface{}的用法。总结来说,interface{} 类似于COM中的IUnknown,我们刚开始对其一无所知,但我们可以通过接口查询和类型查询逐步了解它。
我们说,Go 语言的接口(interface)不单单只是接口。在其他语言中,接口仅仅作为组件间的契约存在。从这个层面讲,Go语言接口的重要突破是,其接口是非侵入式的,把其他语言接口的副作用消除了。
但是Go语言的接口不仅仅是契约作用。它是Go语言类型系统(type system)的纲。这表现在: