- 浏览: 382574 次
- 性别:
- 来自: 北京
文章分类
最新评论
-
lhbthanks:
楼主写的很多,也很实用,要是再增加一些描述就会更好了。
oracle 用户 从一个表空间 另一个表空间 -
wuhuajun:
private int _connectionMax = 51 ...
resin jboss 最大连接数设置 -
shixiaomu:
自己丁丁丁一下 学了忘忘了再学。。主要是应用场景太少
python -
shixiaomu:
我自己有了方案了java+rabbitmq_server-2. ...
hadoop hive zookeeper 还不够 -
shixiaomu:
看到这个帖子 羞愧极了 ,原来 我 09 年就想学 pytho ...
python
go-不为学go,看懂了能大大增强自己的知识面
面向对象编程
Go 语言的面向对象编程(OOP)非常简洁而优雅。说它简洁,简介之处在于,它没有了OOP中很多概念,比如:继承、虚函数、构造函数和析构函数、隐藏的this指针等等。说它优雅,是它的面向对象(OOP)是语言类型系统(type system)中的天然的一部分。整个类型系统通过接口(interface)串联,浑然一体。
类型系统(type system)
很少有编程类的书籍谈及类型系统(type system)这个话题。但实际上类型系统是整个语言的支撑,至关重要。
类型系统(type system)是指一个语言的类型体系图。在整个类型体系图中,包含这些内容:
基本类型。如byte、int、bool、float等等。
复合类型。如数组(array)、结构体(struct)、指针(pointer)等。
Any类型。即可以指向任意对象的类型。
值语义和引用语义。
面向对象。即所有具备面向对象特征(比如有成员方法)的类型。
接口(interface)。
类型系统(type system)描述的是这些内容在一个语言中如何被关联。比如我们聊聊Java的类型系统: 在Java语言中,存在两套完全独立的类型系统,一套是值类型系统,主要是基本类型,如byte、int、boolean、char、double、String等,这些类型基于值语义。一套是以Object类型为根的对象类型系统,这些类型可以定义成员变量、成员方法、可以有虚函数。这些类型基于引用语义,只允许new出来(只允许在堆上)。只有对象类型系统中的实例可以被Any类型引用。Any类型就是整个对象类型系统的根 —— Object类型。值类型想要被Any类型引用,需要装箱(Boxing)过程,比如int类型需要装箱成为Integer类型。只有对象类型系统中的类型才可以实现接口(方法是让该类型从要实现的接口继承)。
在Go语言中,多数类型都是值语义,并且都可以有方法。在需要的时候,你可以给任何类型(包括内置类型)“增加”新方法。实现某个接口(interface)无需从该接口继承(事实上Go语言并没有继承语法),而只需要实现该接口要求的所有方法。任何类型都可以被Any类型引用。Any类型就是空接口,亦即 interface{}。 让我们一一道来。
给类型增加方法
在Go语言中,你可以给任意类型(包括内置类型,但指针类型除外)增加方法,例如:
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
在这个例子中,我们定义了一个新类型Integer,它和int没有本质不同,只是它为内置的int类型增加了个新方法:Less。如此,你就可以让整型看起来像个类那样用:
func main() {
var a Integer = 1
if a.Less(2) {
fmt.Println(a, "Less 2")
}
}
在学其他语言的时候,很多初学者对面向对象感到很神秘。我在给初学者介绍面向对象的时候,经常说到“面向对象只是一个语法糖”。以上代码用面向过程的方式来写是这样的:
type Integer int
func Integer_Less(a Integer, b Integer) bool {
return a < b
}
func main() {
var a Integer = 1
if Integer_Less(a, 2) {
fmt.Println(a, "Less 2")
}
}
在Go语言中,面向对象的神秘面纱被剥得一干二净。对比这两段代码:
func (a Integer) Less(b Integer) bool { // 面向对象
return a < b
}
func Integer_Less(a Integer, b Integer) bool { // 面向过程
return a < b
}
a.Less(2) // 面向对象
Integer_Less(a, 2) // 面向过程
你可以看出,面向对象只是换了一种语法形式来表达。在Go语言中没有隐藏的this指针。这句话的含义是:
第一,方法施加的目标(也就是“对象”)显式传递,没有被隐藏起来。
第二,方法施加的目标(也就是“对象”)不需要非得是指针,也不用非得叫this。
我们对比Java语言的代码:
class Integer {
private int val;
public boolean Less(Integer b) {
return this.val < b.val;
}
}
这段Java代码初学者会比较难懂,主要是因为Integer类的Less方法隐藏了第一个参数Integer* this。如果将其翻译成C代码,会更清晰:
struct Integer {
int val;
};
bool Integer_Less(Integer* this, Integer* b) {
return this->val < b->val;
}
在Go语言中的面向对象最为直观,也无需支付额外的成本。如果要求对象必须以指针传递,这有时会是个额外成本,因为对象有时很小(比如4个字节),用指针传递并不划算。
只有在你需要修改对象的时候,才必须用指针。它不是Go语言的约束,而是一种自然约束。举个例子:
func (a *Integer) Add(b Integer) {
*a += b
}
这里为Integer类型增加了Add方法。由于Add方法需要修改对象的值,所以需要用指针引用。调用如下:
func main() {
var a Integer = 1
a.Add(2)
fmt.Println("a =", a)
}
运行该程序得到的结果是:a = 3。如果你不用指针:
func (a Integer) Add(b Integer) {
a += b
}
运行程序得到的结果是:a = 1,也就是维持原来的值。究其原因,是因为Go和C语言一样,类型都是基于值传递。要想修改变量的值,只能传递指针。
值语义和引用语义
值语义和引用语义的差别在于赋值:
b = a
b.Modify()
如果b的修改不会影响a的值,那么此类型属于值类型。如果会影响a的值,那么此类型是引用类型。
多数Go语言中的类型,包括:
基本类型。如byte、int、bool、float32、float64、string等等。
复合类型。如数组(array)、结构体(struct)、指针(pointer)等。
都基于值语义。Go语言中类型的值语义表现得非常彻底。我们这么说是因为数组(array)。如果你学习过C语言,你会知道C语言中的数组(array)比较特别。通过函数传递一个数组的时候基于引用语义,但是在结构体中定义数组变量的时候是值语义(表现在结构体赋值的时候,该数组会被完整地拷贝一份新的副本)。
Go语言中的数组(array)和基本类型没有区别,是很纯粹的值类型。例如:
var a = [3]int{1, 2, 3}
var b = a
b[1]++
fmt.Println(a, b)
程序运行结果:[1 2 3] [1 3 3]。这表明b = a赋值语句是数组内容的完整拷贝。要想表达引用,需要用指针:
var a = [3]int{1, 2, 3}
var b = &a
b[1]++
fmt.Println(a, *b)
程序运行结果:[1 3 3] [1 3 3]。这表明b=&a赋值语句是数组内容的引用。变量b的类型不是[3]int,而是*[3]int类型。
Go语言中有4个类型比较特别,看起来像引用类型:
切片(slice):指向数组(array)的一个区间。
字典(map):极其常见的数据结构,提供key-value查询能力。
通道(chan):执行体(goroutine)间通讯设施。
接口(interface):对一组满足某个契约的类型的抽象。
但是这并不影响我们将Go语言类型是值语义的本质。我们一个个来看这些类型:
切片(slice)本质上是range,你可以大致将 []T 表示为:
type slice struct {
first *T
last *T
end *T
}
因为切片(slice)内部是一系列的指针,所以可以改变所指向的数组(array)的元素并不奇怪。slice类型本身的赋值仍然是值语义。
字典(map)本质上是一个字典指针,你可以大致将map[K]V表示为:
type Map_K_V struct {
...
}
type map[K]V struct {
impl *Map_K_V
}
基于指针(pointer),我们完全可以自定义一个引用类型,如:
type IntegerRef struct { impl *int }
通道(chan)和字典(map)类似,本质上是一个指针。为什么将他们设计为是引用类型而不是统一的值类型,是因为完整拷贝一个通道(chan)或字典(map)并是常规需求。
同样,接口(interface)具备引用语义,是因为内部维持了两个指针。示意为:
type interface struct {
data *void
itab *Itab
}
接口在Go语言中的地位非常重要。关于接口(interface)内部实现细节,后面在高阶话题中,我们再细细剖析。
结构体(struct)
Go语言的结构体(struct)和其它语言的类(class)有同等的地位。但Go语言放弃了包括继承在内的大量OOP特性,只保留了组合(compose)这个最基础的特性。
组合(compose)甚至不能算OOP的特性。因为连C语言这样的过程式编程语言中,也有结构体(struct),也有组合(compose)。组合只是形成复合类型的基础。
上面我们说到,所有的Go语言的类型(指针类型除外)都是可以有自己的方法。在这个背景下,Go语言的结构体(struct)它只是很普通的复合类型,平淡无奇。例如我们要定义一个矩形类型:
type Rect struct {
x, y float64
width, height float64
}
然后我们定义方法Area来计算矩形的面积:
func (r *Rect) Area() float64 {
return r.width * r.height
}
初始化
定义了Rect类型后,我们如何创建并初始化Rect类型的对象实例?有如下方法:
rect1 := new(Rect)
rect2 := &Rect{}
rect3 := &Rect{0, 0, 100, 200}
rect4 := &Rect{width: 100, height: 200}
在Go语言中,未显式进行初始化的变量,都会初始化为该类型的零值(例如对于bool类型的零值为false,对于int类型零值为0,对于string类型零值为空字符串)。
构造函数?不需要。在Go语言中你只需要定义一个普通的函数,只是通常以NewXXX来命名,表示“构造函数”:
func NewRect(x, y, width, height float64) *Rect {
return &Rect{x, y, width, height}
}
这一切非常自然,没有任何突兀之处。
匿名组合
确切地说,Go语言也提供了继承,但是采用了组合的文法,我们称之为匿名组合:
type Base struct {
...
}
func (base *Base) Foo() { ... }
func (base *Base) Bar() { ... }
type Foo struct {
Base
...
}
func (foo *Foo) Bar() {
foo.Base.Bar()
...
}
以上代码定义了一个Base类(实现了Foo、Bar两个成员方法),然后定义了一个Foo类,从 Base“继承”并实现了改写了Bar方法,该方法实现时先调用了基类的Bar方法。
在“派生类”Foo没有改写“基类”Base的成员方法时,相应的方法就被“继承”。例如在上面的例子中,调用foo.Foo() 和调用foo.Base.Foo() 效果一致。
区别于其他语言,Go语言很清晰地告诉你类的内存布局是怎么样的。在Go语言中你还可以随心所欲地修改内存布局,如:
type Foo struct {
...
Base
}
这段代码从语义上来说,和上面给例子并无不同,但内存布局发生了改变。“基类”Base的数据被放在了“派生类”Foo 的最后。
另外,在Go语言中你还可以以指针方式从一个类“派生”:
type Foo struct {
*Base
...
}
这段Go代码仍然有“派生”的效果,只是Foo创建实例的时候,需要外部提供一个Base类实例的指针。C++ 中其实也有类似的功能,那就是虚基类。但是虚基类是非常让人难以理解的特性,普遍上来说 C++ 的开发者都会遗忘这个特性。
成员的可访问性
Go语言对关键字的增加非常吝啬。在Go语言中没有private、protected、public这样的关键字。要想某个符号可被其他包(package)访问,需要将该符号定义为大写字母开头。如:
type Rect struct {
X, Y float64
Width, Height float64
}
这样,Rect类型的成员变量就全部被public了。成员方法遵循同样的规则,例如:
func (r *Rect) area() float64 {
return r.Width * r.Height
}
这样,Rect的area方法只能在该类型所在的包(package)内使用。
需要强调的一点是,Go语言中符号的可访问性是包(package)一级的,而不是类一级的。尽管area是Rect的内部方法,但是在同一个包中的其他类型可以访问到它。这样的可访问性控制很粗旷,很特别,但是非常实用。如果Go语言符号的可访问性是类一级的,少不了还要加上friend这样的关键字,以表示两个类是朋友关系,可以访问其中的私有成员。
接口(interface)
Rob Pike曾经说,如果只能选择一个Go语言的特性移植到其他语言中,他会选择接口。
接口(interface)在Go语言有着至关重要的地位。如果说goroutine和channel 是支撑起Go语言的并发模型的基石,让Go语言在如今集群化与多核化的时代,成为一道极为亮丽的风景;那么接口(interface)是Go语言整个类型系统(type system)的基石,让Go语言在基础编程哲学的探索上,达到史无先例的高度。
我曾在多个场合说,Go语言在编程哲学上是变革派,而不是改良派。这不是因为Go语言有 goroutine和channel,而更重要的是因为Go语言的类型系统,因为Go语言的接口。因为有接口,才让Go语言的编程哲学变得完美。
Go 语言的接口(interface)不单单只是接口。
为什么这么说?让我们细细道来。
其他语言(C++/Java/C#)的接口
Go语言的接口,并不是你之前在其他语言(C++/Java/C#等)中接触到的接口。
在Go语言之前的接口(interface),主要作为不同组件之间的契约存在。对契约的实现是强制的,你必须声明你的确实现了该接口。为了实现一个接口,你需要从该接口继承:
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语言的文法来描述要实现的方法,请忽略文法上的细节),它有这些方法:
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语言中,一个类只需要实现了接口要求的所有函数,那么我们就说这个类实现了该接口。例如:
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中,类的继承树并无意义。你只需要知道这个类实现了哪些方法,每个方法是啥含义就足够了。
其二,实现类的时候,只需要关心自己应该提供哪些方法。不用再纠结接口需要拆得多细才合理。接口是由使用方按需定义,而不用事前规划。
其三,不用为了实现一个接口而import一个包,目的仅仅是引用其中的某个interface的定义,这是不被推荐的。因为多引用一个外部的package,就意味着更多的耦合。接口由使用方按自身需求来定义,使用方无需关心是否有其他模块定义过类似的接口。
接口赋值
接口(interface)的赋值在Go语言中分为如下2种情况讨论:
将对象实例赋值给接口
将接口赋值给另一个接口
先讨论将某种类型的对象实例赋值给接口。这要求该对象实例实现了接口要求的所有方法。例如,在之前我们有实作过一个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)
}
因为 (&a).Add改变的只是函数参数a,对外部实际要操作的对象并无影响,这不符合用户的预期。故此,Go语言不会自动为其生成该函数。因此,类型Integer只存在Less方法,缺少Add方法,不满足LessAdder接口,故此上面的语句(2)不能赋值。
为了进一步证明以上的推理,我们不妨再定义一个Lesser接口,如下:
type Lesser interface {
Less(b Integer) bool
}
然后我们定义一个Integer类型的对象实例,将其赋值给Lesser接口:
var a Integer = 1
var b1 Lesser = &a ... (1)
var b2 Lesser = a ... (2)
正如如我们所料的那样,语句(1)和语句(2)均可以编译通过。
我们再来讨论另一种情形:将接口赋值给另一个接口。在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)
}
这里我们定义了两个接口,一个叫 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并无差异。
以下这些代码可编译通过:
var file1 two.IStream = new(File)
var file2 one.ReadWriter = file1
var file3 two.IStream = file2
接口赋并不要求两个接口必须等价。如果接口A方法列表是接口B方法列表的子集,那么接口B可以赋值给接口A。例如假设我们有Writer接口:
type Writer interface {
Write(buf []byte) (n int, err error)
}
我们可以将上面的one.ReadWriter、two.IStream接口的实例赋值给Writer接口:
var file1 two.IStream = new(File)
var file4 Writer = file1
但是反过来并不成立:
var file1 Writer = new(File)
var file5 two.IStream = file1 // 编译不能通过!
这段代码无法编译通过。原因是显然的:file1并没有Read方法。
接口查询
有办法让上面Writer接口转换为two.IStream接口么?有。那就是我们即将讨论的接口查询语法。代码如下:
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语言中,你可以向接口询问,它指向的对象是否是某个类型,例子如下:
var file1 Writer = ...
if file6, ok := file1.(*File); ok {
...
}
这个if语句的含义是:file1接口指向的对象实例是否是 *File 类型呢?如果是的,则...
你可以认为查询接口所指向的对象是否是某个类型,只是接口查询的一个特例。接口是对一组类型的公共特性的抽象。所以查询接口与查询具体类型的区别,好比是下面这两句问话的区别:
> 你是医生吗?
> 是。
> 你是某某某?
> 是。
第一句问话查的是一个群体,是查询接口;而第二句问话已经到了具体的个体,是查询具体类型。
在C++/Java/C# 等语言中,也有一些类似的动态查询能力,比如查询一个对象的类型是否是继承自某个类型(基类查询),或者是否实现了某个接口(接口派生查询)。但是他们的动态查询与Go的动态查询很不一样。
> 你是医生吗?
对于这个问题,基类查询看起来像是在这么问:“你老爸是医生吗?”;接口派生查询则看起来像是这么问:“你有医师执照吗?”;在Go语言中,则是先确定满足什么样的条件才是医生,比如技能要求有哪些,然后才是按条件一一拷问,确认是否满足条件,只要满足了你就是医生,不关心你是否有医师执照,或者是小国执照不被天朝承认。
类型查询
在Go语言中,你还可以更加直接了当地询问接口指向的对象实例的类型。例如:
var v1 interface{} = ...
switch v := v1.(type) {
case int: // 现在v的类型是int
case string: // 现在v的类型是string
...
}
就像现实生活中物种多得数不清一样,语言中的类型也多的数不清。所以类型查询并不经常被使用。它更多看起来是个补充,需要配合接口查询使用。例如:
type Stringer interface {
String() string
}
func Println(args ...interface{}) {
for _, arg := range args {
switch v := v1.(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()方法转换为字符串进行打印。否则,Println利用反射(reflect)遍历对象的所有成员变量进行打印。
是的,利用反射(reflect)也可以进行类型查询,详细可参阅reflect.TypeOf方法相关文档。在后文高阶话题中我们也会探讨有关“反射(reflect)”的话题。
Any类型
由于Go语言中任何对象实例都满足空接口interface{},故此interface{}看起来像是可以指向任何对象的Any类型。如下:
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系列的函数。例如:
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{}可指向任意的对象实例。
Go 语言的面向对象编程(OOP)非常简洁而优雅。说它简洁,简介之处在于,它没有了OOP中很多概念,比如:继承、虚函数、构造函数和析构函数、隐藏的this指针等等。说它优雅,是它的面向对象(OOP)是语言类型系统(type system)中的天然的一部分。整个类型系统通过接口(interface)串联,浑然一体。
类型系统(type system)
很少有编程类的书籍谈及类型系统(type system)这个话题。但实际上类型系统是整个语言的支撑,至关重要。
类型系统(type system)是指一个语言的类型体系图。在整个类型体系图中,包含这些内容:
基本类型。如byte、int、bool、float等等。
复合类型。如数组(array)、结构体(struct)、指针(pointer)等。
Any类型。即可以指向任意对象的类型。
值语义和引用语义。
面向对象。即所有具备面向对象特征(比如有成员方法)的类型。
接口(interface)。
类型系统(type system)描述的是这些内容在一个语言中如何被关联。比如我们聊聊Java的类型系统: 在Java语言中,存在两套完全独立的类型系统,一套是值类型系统,主要是基本类型,如byte、int、boolean、char、double、String等,这些类型基于值语义。一套是以Object类型为根的对象类型系统,这些类型可以定义成员变量、成员方法、可以有虚函数。这些类型基于引用语义,只允许new出来(只允许在堆上)。只有对象类型系统中的实例可以被Any类型引用。Any类型就是整个对象类型系统的根 —— Object类型。值类型想要被Any类型引用,需要装箱(Boxing)过程,比如int类型需要装箱成为Integer类型。只有对象类型系统中的类型才可以实现接口(方法是让该类型从要实现的接口继承)。
在Go语言中,多数类型都是值语义,并且都可以有方法。在需要的时候,你可以给任何类型(包括内置类型)“增加”新方法。实现某个接口(interface)无需从该接口继承(事实上Go语言并没有继承语法),而只需要实现该接口要求的所有方法。任何类型都可以被Any类型引用。Any类型就是空接口,亦即 interface{}。 让我们一一道来。
给类型增加方法
在Go语言中,你可以给任意类型(包括内置类型,但指针类型除外)增加方法,例如:
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
在这个例子中,我们定义了一个新类型Integer,它和int没有本质不同,只是它为内置的int类型增加了个新方法:Less。如此,你就可以让整型看起来像个类那样用:
func main() {
var a Integer = 1
if a.Less(2) {
fmt.Println(a, "Less 2")
}
}
在学其他语言的时候,很多初学者对面向对象感到很神秘。我在给初学者介绍面向对象的时候,经常说到“面向对象只是一个语法糖”。以上代码用面向过程的方式来写是这样的:
type Integer int
func Integer_Less(a Integer, b Integer) bool {
return a < b
}
func main() {
var a Integer = 1
if Integer_Less(a, 2) {
fmt.Println(a, "Less 2")
}
}
在Go语言中,面向对象的神秘面纱被剥得一干二净。对比这两段代码:
func (a Integer) Less(b Integer) bool { // 面向对象
return a < b
}
func Integer_Less(a Integer, b Integer) bool { // 面向过程
return a < b
}
a.Less(2) // 面向对象
Integer_Less(a, 2) // 面向过程
你可以看出,面向对象只是换了一种语法形式来表达。在Go语言中没有隐藏的this指针。这句话的含义是:
第一,方法施加的目标(也就是“对象”)显式传递,没有被隐藏起来。
第二,方法施加的目标(也就是“对象”)不需要非得是指针,也不用非得叫this。
我们对比Java语言的代码:
class Integer {
private int val;
public boolean Less(Integer b) {
return this.val < b.val;
}
}
这段Java代码初学者会比较难懂,主要是因为Integer类的Less方法隐藏了第一个参数Integer* this。如果将其翻译成C代码,会更清晰:
struct Integer {
int val;
};
bool Integer_Less(Integer* this, Integer* b) {
return this->val < b->val;
}
在Go语言中的面向对象最为直观,也无需支付额外的成本。如果要求对象必须以指针传递,这有时会是个额外成本,因为对象有时很小(比如4个字节),用指针传递并不划算。
只有在你需要修改对象的时候,才必须用指针。它不是Go语言的约束,而是一种自然约束。举个例子:
func (a *Integer) Add(b Integer) {
*a += b
}
这里为Integer类型增加了Add方法。由于Add方法需要修改对象的值,所以需要用指针引用。调用如下:
func main() {
var a Integer = 1
a.Add(2)
fmt.Println("a =", a)
}
运行该程序得到的结果是:a = 3。如果你不用指针:
func (a Integer) Add(b Integer) {
a += b
}
运行程序得到的结果是:a = 1,也就是维持原来的值。究其原因,是因为Go和C语言一样,类型都是基于值传递。要想修改变量的值,只能传递指针。
值语义和引用语义
值语义和引用语义的差别在于赋值:
b = a
b.Modify()
如果b的修改不会影响a的值,那么此类型属于值类型。如果会影响a的值,那么此类型是引用类型。
多数Go语言中的类型,包括:
基本类型。如byte、int、bool、float32、float64、string等等。
复合类型。如数组(array)、结构体(struct)、指针(pointer)等。
都基于值语义。Go语言中类型的值语义表现得非常彻底。我们这么说是因为数组(array)。如果你学习过C语言,你会知道C语言中的数组(array)比较特别。通过函数传递一个数组的时候基于引用语义,但是在结构体中定义数组变量的时候是值语义(表现在结构体赋值的时候,该数组会被完整地拷贝一份新的副本)。
Go语言中的数组(array)和基本类型没有区别,是很纯粹的值类型。例如:
var a = [3]int{1, 2, 3}
var b = a
b[1]++
fmt.Println(a, b)
程序运行结果:[1 2 3] [1 3 3]。这表明b = a赋值语句是数组内容的完整拷贝。要想表达引用,需要用指针:
var a = [3]int{1, 2, 3}
var b = &a
b[1]++
fmt.Println(a, *b)
程序运行结果:[1 3 3] [1 3 3]。这表明b=&a赋值语句是数组内容的引用。变量b的类型不是[3]int,而是*[3]int类型。
Go语言中有4个类型比较特别,看起来像引用类型:
切片(slice):指向数组(array)的一个区间。
字典(map):极其常见的数据结构,提供key-value查询能力。
通道(chan):执行体(goroutine)间通讯设施。
接口(interface):对一组满足某个契约的类型的抽象。
但是这并不影响我们将Go语言类型是值语义的本质。我们一个个来看这些类型:
切片(slice)本质上是range,你可以大致将 []T 表示为:
type slice struct {
first *T
last *T
end *T
}
因为切片(slice)内部是一系列的指针,所以可以改变所指向的数组(array)的元素并不奇怪。slice类型本身的赋值仍然是值语义。
字典(map)本质上是一个字典指针,你可以大致将map[K]V表示为:
type Map_K_V struct {
...
}
type map[K]V struct {
impl *Map_K_V
}
基于指针(pointer),我们完全可以自定义一个引用类型,如:
type IntegerRef struct { impl *int }
通道(chan)和字典(map)类似,本质上是一个指针。为什么将他们设计为是引用类型而不是统一的值类型,是因为完整拷贝一个通道(chan)或字典(map)并是常规需求。
同样,接口(interface)具备引用语义,是因为内部维持了两个指针。示意为:
type interface struct {
data *void
itab *Itab
}
接口在Go语言中的地位非常重要。关于接口(interface)内部实现细节,后面在高阶话题中,我们再细细剖析。
结构体(struct)
Go语言的结构体(struct)和其它语言的类(class)有同等的地位。但Go语言放弃了包括继承在内的大量OOP特性,只保留了组合(compose)这个最基础的特性。
组合(compose)甚至不能算OOP的特性。因为连C语言这样的过程式编程语言中,也有结构体(struct),也有组合(compose)。组合只是形成复合类型的基础。
上面我们说到,所有的Go语言的类型(指针类型除外)都是可以有自己的方法。在这个背景下,Go语言的结构体(struct)它只是很普通的复合类型,平淡无奇。例如我们要定义一个矩形类型:
type Rect struct {
x, y float64
width, height float64
}
然后我们定义方法Area来计算矩形的面积:
func (r *Rect) Area() float64 {
return r.width * r.height
}
初始化
定义了Rect类型后,我们如何创建并初始化Rect类型的对象实例?有如下方法:
rect1 := new(Rect)
rect2 := &Rect{}
rect3 := &Rect{0, 0, 100, 200}
rect4 := &Rect{width: 100, height: 200}
在Go语言中,未显式进行初始化的变量,都会初始化为该类型的零值(例如对于bool类型的零值为false,对于int类型零值为0,对于string类型零值为空字符串)。
构造函数?不需要。在Go语言中你只需要定义一个普通的函数,只是通常以NewXXX来命名,表示“构造函数”:
func NewRect(x, y, width, height float64) *Rect {
return &Rect{x, y, width, height}
}
这一切非常自然,没有任何突兀之处。
匿名组合
确切地说,Go语言也提供了继承,但是采用了组合的文法,我们称之为匿名组合:
type Base struct {
...
}
func (base *Base) Foo() { ... }
func (base *Base) Bar() { ... }
type Foo struct {
Base
...
}
func (foo *Foo) Bar() {
foo.Base.Bar()
...
}
以上代码定义了一个Base类(实现了Foo、Bar两个成员方法),然后定义了一个Foo类,从 Base“继承”并实现了改写了Bar方法,该方法实现时先调用了基类的Bar方法。
在“派生类”Foo没有改写“基类”Base的成员方法时,相应的方法就被“继承”。例如在上面的例子中,调用foo.Foo() 和调用foo.Base.Foo() 效果一致。
区别于其他语言,Go语言很清晰地告诉你类的内存布局是怎么样的。在Go语言中你还可以随心所欲地修改内存布局,如:
type Foo struct {
...
Base
}
这段代码从语义上来说,和上面给例子并无不同,但内存布局发生了改变。“基类”Base的数据被放在了“派生类”Foo 的最后。
另外,在Go语言中你还可以以指针方式从一个类“派生”:
type Foo struct {
*Base
...
}
这段Go代码仍然有“派生”的效果,只是Foo创建实例的时候,需要外部提供一个Base类实例的指针。C++ 中其实也有类似的功能,那就是虚基类。但是虚基类是非常让人难以理解的特性,普遍上来说 C++ 的开发者都会遗忘这个特性。
成员的可访问性
Go语言对关键字的增加非常吝啬。在Go语言中没有private、protected、public这样的关键字。要想某个符号可被其他包(package)访问,需要将该符号定义为大写字母开头。如:
type Rect struct {
X, Y float64
Width, Height float64
}
这样,Rect类型的成员变量就全部被public了。成员方法遵循同样的规则,例如:
func (r *Rect) area() float64 {
return r.Width * r.Height
}
这样,Rect的area方法只能在该类型所在的包(package)内使用。
需要强调的一点是,Go语言中符号的可访问性是包(package)一级的,而不是类一级的。尽管area是Rect的内部方法,但是在同一个包中的其他类型可以访问到它。这样的可访问性控制很粗旷,很特别,但是非常实用。如果Go语言符号的可访问性是类一级的,少不了还要加上friend这样的关键字,以表示两个类是朋友关系,可以访问其中的私有成员。
接口(interface)
Rob Pike曾经说,如果只能选择一个Go语言的特性移植到其他语言中,他会选择接口。
接口(interface)在Go语言有着至关重要的地位。如果说goroutine和channel 是支撑起Go语言的并发模型的基石,让Go语言在如今集群化与多核化的时代,成为一道极为亮丽的风景;那么接口(interface)是Go语言整个类型系统(type system)的基石,让Go语言在基础编程哲学的探索上,达到史无先例的高度。
我曾在多个场合说,Go语言在编程哲学上是变革派,而不是改良派。这不是因为Go语言有 goroutine和channel,而更重要的是因为Go语言的类型系统,因为Go语言的接口。因为有接口,才让Go语言的编程哲学变得完美。
Go 语言的接口(interface)不单单只是接口。
为什么这么说?让我们细细道来。
其他语言(C++/Java/C#)的接口
Go语言的接口,并不是你之前在其他语言(C++/Java/C#等)中接触到的接口。
在Go语言之前的接口(interface),主要作为不同组件之间的契约存在。对契约的实现是强制的,你必须声明你的确实现了该接口。为了实现一个接口,你需要从该接口继承:
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语言的文法来描述要实现的方法,请忽略文法上的细节),它有这些方法:
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语言中,一个类只需要实现了接口要求的所有函数,那么我们就说这个类实现了该接口。例如:
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中,类的继承树并无意义。你只需要知道这个类实现了哪些方法,每个方法是啥含义就足够了。
其二,实现类的时候,只需要关心自己应该提供哪些方法。不用再纠结接口需要拆得多细才合理。接口是由使用方按需定义,而不用事前规划。
其三,不用为了实现一个接口而import一个包,目的仅仅是引用其中的某个interface的定义,这是不被推荐的。因为多引用一个外部的package,就意味着更多的耦合。接口由使用方按自身需求来定义,使用方无需关心是否有其他模块定义过类似的接口。
接口赋值
接口(interface)的赋值在Go语言中分为如下2种情况讨论:
将对象实例赋值给接口
将接口赋值给另一个接口
先讨论将某种类型的对象实例赋值给接口。这要求该对象实例实现了接口要求的所有方法。例如,在之前我们有实作过一个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)
}
因为 (&a).Add改变的只是函数参数a,对外部实际要操作的对象并无影响,这不符合用户的预期。故此,Go语言不会自动为其生成该函数。因此,类型Integer只存在Less方法,缺少Add方法,不满足LessAdder接口,故此上面的语句(2)不能赋值。
为了进一步证明以上的推理,我们不妨再定义一个Lesser接口,如下:
type Lesser interface {
Less(b Integer) bool
}
然后我们定义一个Integer类型的对象实例,将其赋值给Lesser接口:
var a Integer = 1
var b1 Lesser = &a ... (1)
var b2 Lesser = a ... (2)
正如如我们所料的那样,语句(1)和语句(2)均可以编译通过。
我们再来讨论另一种情形:将接口赋值给另一个接口。在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)
}
这里我们定义了两个接口,一个叫 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并无差异。
以下这些代码可编译通过:
var file1 two.IStream = new(File)
var file2 one.ReadWriter = file1
var file3 two.IStream = file2
接口赋并不要求两个接口必须等价。如果接口A方法列表是接口B方法列表的子集,那么接口B可以赋值给接口A。例如假设我们有Writer接口:
type Writer interface {
Write(buf []byte) (n int, err error)
}
我们可以将上面的one.ReadWriter、two.IStream接口的实例赋值给Writer接口:
var file1 two.IStream = new(File)
var file4 Writer = file1
但是反过来并不成立:
var file1 Writer = new(File)
var file5 two.IStream = file1 // 编译不能通过!
这段代码无法编译通过。原因是显然的:file1并没有Read方法。
接口查询
有办法让上面Writer接口转换为two.IStream接口么?有。那就是我们即将讨论的接口查询语法。代码如下:
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语言中,你可以向接口询问,它指向的对象是否是某个类型,例子如下:
var file1 Writer = ...
if file6, ok := file1.(*File); ok {
...
}
这个if语句的含义是:file1接口指向的对象实例是否是 *File 类型呢?如果是的,则...
你可以认为查询接口所指向的对象是否是某个类型,只是接口查询的一个特例。接口是对一组类型的公共特性的抽象。所以查询接口与查询具体类型的区别,好比是下面这两句问话的区别:
> 你是医生吗?
> 是。
> 你是某某某?
> 是。
第一句问话查的是一个群体,是查询接口;而第二句问话已经到了具体的个体,是查询具体类型。
在C++/Java/C# 等语言中,也有一些类似的动态查询能力,比如查询一个对象的类型是否是继承自某个类型(基类查询),或者是否实现了某个接口(接口派生查询)。但是他们的动态查询与Go的动态查询很不一样。
> 你是医生吗?
对于这个问题,基类查询看起来像是在这么问:“你老爸是医生吗?”;接口派生查询则看起来像是这么问:“你有医师执照吗?”;在Go语言中,则是先确定满足什么样的条件才是医生,比如技能要求有哪些,然后才是按条件一一拷问,确认是否满足条件,只要满足了你就是医生,不关心你是否有医师执照,或者是小国执照不被天朝承认。
类型查询
在Go语言中,你还可以更加直接了当地询问接口指向的对象实例的类型。例如:
var v1 interface{} = ...
switch v := v1.(type) {
case int: // 现在v的类型是int
case string: // 现在v的类型是string
...
}
就像现实生活中物种多得数不清一样,语言中的类型也多的数不清。所以类型查询并不经常被使用。它更多看起来是个补充,需要配合接口查询使用。例如:
type Stringer interface {
String() string
}
func Println(args ...interface{}) {
for _, arg := range args {
switch v := v1.(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()方法转换为字符串进行打印。否则,Println利用反射(reflect)遍历对象的所有成员变量进行打印。
是的,利用反射(reflect)也可以进行类型查询,详细可参阅reflect.TypeOf方法相关文档。在后文高阶话题中我们也会探讨有关“反射(reflect)”的话题。
Any类型
由于Go语言中任何对象实例都满足空接口interface{},故此interface{}看起来像是可以指向任何对象的Any类型。如下:
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系列的函数。例如:
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{}可指向任意的对象实例。
相关推荐
2. **学习建议句型"Let's go and have a look."**:通过这个句型,学生能学会提出参观或查看某事物的建议,增强他们的交流能力。 3. **理解并使用表示方位的词"near"**:教学过程中,教师将引导学生理解并正确使用...
)、`Let’s go and see.`(我们去看看吧。)、`Where is it?`(它在哪里?)、`It’s near the window.`(它在窗户附近。)、`Let’s clean the classroom.`(让我们打扫教室吧。)、`Let me clean the teacher’s ...
1. 教学内容与难度调整:从试卷分析来看,试题过于侧重基础识记性知识,缺乏对理解和应用能力的测试。教师在出题时避免难题和复杂题目,导致试题整体难度偏低。这反映出教师在设计试题时需要更加平衡识记与理解的...
在IT行业中,识别一个真正的程序员并不只看他们的年龄或简历,更重要的是他们对技术的热情、理解和应用能力。本文将深入探讨这一主题,帮助你理解如何判断一个人是否具备真正的程序员素养。 首先,真正的程序员通常...
(我们不能总是使用别人设定好的框架,为什么不能有我们自己的框架?) 因此,首先你必须掌握 GoF 的设计模式。虽然它是隐性,但不是可以越过的。 关于本站“设计模式” Java 提供了丰富的 API,同时又有强大的...
实习者跟随一名熟练车工,学习了从裁剪到缝制成衣的全过程,包括看懂样品需求表,独立完成样衣制作,以及进行品质检验。在品质检验阶段,实习者掌握了尺寸测量和外观质量检查,包括口袋、里布、线头、跳线等问题的...
卷序列号为 64ED-8C1D D:\我的酷盘\FTP\学员面试资料 │ 164个完整Java代码.zip │ J2EE综合--Struts常见错误的全面汇总.txt │ java程序员面试资料.zip │ JAVA笔试题(上海释锐).pdf │ MIME简介.txt │ SCJP...
卷序列号为 64ED-8C1D D:\我的酷盘\FTP\学员面试资料 │ 164个完整Java代码.zip │ J2EE综合--Struts常见错误的全面汇总.txt │ java程序员面试资料.zip │ JAVA笔试题(上海释锐).pdf │ MIME简介.txt │ SCJP...
卷序列号为 64ED-8C1D D:\我的酷盘\FTP\学员面试资料 │ 164个完整Java代码.zip │ J2EE综合--Struts常见错误的全面汇总.txt │ java程序员面试资料.zip │ JAVA笔试题(上海释锐).pdf │ MIME简介.txt │ SCJP...
卷序列号为 64ED-8C1D D:\我的酷盘\FTP\学员面试资料 │ 164个完整Java代码.zip │ J2EE综合--Struts常见错误的全面汇总.txt │ java程序员面试资料.zip │ JAVA笔试题(上海释锐).pdf │ MIME简介.txt │ SCJP...
卷序列号为 64ED-8C1D D:\我的酷盘\FTP\学员面试资料 │ 164个完整Java代码.zip │ J2EE综合--Struts常见错误的全面汇总.txt │ java程序员面试资料.zip │ JAVA笔试题(上海释锐).pdf │ MIME简介.txt │ SCJP...
此外,广泛的知识面、会计电算化技能、英语水平和沟通能力也是不可或缺的。 2. 对学院教学的建议是,应注重培养既懂理论又有实践能力的复合型人才,加强会计电算化的教学,并通过与企业的合作,提高学生的实际操作...