(整理自网络)

面向对象编程

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的更多相关文章

  1. 面向对象编程语言中的接口(Interface)

    在大多面向对象的编程语言中都提供了Interface(接口)的概念.如果你事先学过这个概念,那么在谈到“接口测试”时,会不会想起这个概念来!?本篇文章简单介绍一下面向对象编程语言中的Interface ...

  2. JavaScript - 基于原型的面向对象

    JavaScript - 基于原型的面向对象 1. 引言 JavaScript 是一种基于原型的面向对象语言,而不是基于类的!!! 基于类的面向对象语言,比如 Java,是构建在两个不同实体的概念之上 ...

  3. Java是一门面向对象编程语言的理解

    Java是一门面向对象编程语言. 不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承.指针等概念,因此Java语言具有功能强大和简单易用两个特征. Java语言作为静态面向对象编程语言的 ...

  4. R语言基于S4的面向对象编程

    前言 本文接上一篇文章 R语言基于S3的面向对象编程,本文继续介绍R语言基于S4的面向对象编程. S4对象系统具有明显的结构化特征,更适合面向对象的程序设计.Bioconductor社区,以S4对象系 ...

  5. js是函数式的面向对象编程语言

    js是函数式的面向对象编程语言,而非类式的面向对象编程语言

  6. ubuntu基于VSCode的C++编程语言的构建调试环境搭建指南

    ubuntu基于VSCode的C++编程语言的构建调试环境搭建指南 首先安装g++ sudo apt install g++ 检查是否安装成功: 在插件栏安装插件c/c++.code runner: ...

  7. javascript: 基于原型的面向对象编程

    Douglas Crockford指出javascript是世界上最被误解的编程语言.由于javascript缺少常见的面向对象概念,许多程序猿认为javascript不是一个合适的语言.我在做第一个 ...

  8. javascript基于原型实现面向对象

    传统的OO语言有类的概念,但js(ES5)却是基于原型实现的面向对象. 原型是?我们创建的每一个函数都会有一个原型(prototype)属性,这个属性是一个指针,指向函数的原型(prototype)对 ...

  9. JS是面向过程、面向对象还是基于对象?面向对象的代码体现

    一.问题 javascript是面向对象的,还是面向过程的?基于对象是什么意思? 对象: 指的是对某一类事物进行抽象,抽象出这一类事物共同的特征以及行为(也就是属性和方法),那些拥有这一共同属性和方法 ...

随机推荐

  1. 我心中的核心组件(可插拔的AOP)~消息组件~完善篇

    回到目录 为什么要有本篇文章 本篇文章主要实现了RTX消息生产者,并且完成了整体的设计方式,之前在设计时消息生产者全局使用单一的生产方式,即一个项目里使用了Email就不能使用SMS,这种设计方法和实 ...

  2. Java中static的理解

    static表示"全局"或者"静态"的意思,用来修饰成员变量和成员方法,也可以形成静态static代码块,但是Java语言中没有全局变量的概念. 被static ...

  3. Atitti 数据库事务处理 attilax总结

    Atitti 数据库事务处理 attilax总结 1.1. 为什么要传递Connection?1 1.2. 两种事务处理方式,一种是编程式事务处理;一种是声明...2 1.3. 事务隔离级别 2 1. ...

  4. Hadoop学习笔记【Hadoop家族成员概述】

    Hadoop家族成员概述 一.Hadoop简介 1.1 什么是Hadoop? Hadoop是一个分布式系统基础架构,由Apache基金会所开发,目前Yahoo!是其最重要的贡献者. Hadoop实现了 ...

  5. China Mobile 免流原理

    用户添加包头之类(SSR软件),最后连接上了Server. 这个过程因为修改了数据包(混淆)以及服务器使用移动IP. 服务器得到信息后通过计费系统漏洞137 138端口等再返回给用户. 整个过程最重要 ...

  6. angularJS自定义那些事

    angularJS在数据处理方面很优秀. 使用angularJ给我感觉就像在写模板,然后对模板填入内容,只是这些内容不在是 在html页面编写,而是以数据的方式添加进去,这个也大大提高了编写的效率. ...

  7. CSS图片裁剪Clip

    CSS Clip 裁剪 可以用它来裁剪一张图片噢. 我们来裁剪一下Google图标,把G字给裁剪下来. 注意这个clip得配合绝对定位才能使用,不然不起效果. rect (top, right, bo ...

  8. javascript运算符——算术运算符

    × 目录 [1]一元加 [2]一元减 [3]递增[4]递减[5]加法[6]减法[7]乘法[8]除法[9]求余 前面的话 javascript中的算术操作主要通过算术运算符来实现,本文将介绍算术运算符的 ...

  9. Android基于mAppWidget实现手绘地图(二)--概要

    离线地图是一张被切成类似瓷砖般格子图像组成,这些被切开的格子组织成多个缩放级别.缩放级别是从0开始.0这个缩放级别,地图图像的尺寸为1*1像素.在每下一个缩放级别,图像尺寸则会两倍递增. 每个地图都是 ...

  10. Java开发中的高频Collections用法总结与Java平台实现源代码查看方式

    一生二,二生三,三生万物,基础永远是一个计算机人的立身之本,相信看到这篇文章的人一般都知道数据结构这门课程,要不也不会找到我的这篇文章.数据结构这门课程的分析奠定了工程师对各种平台中的容器类,集合类的 ...