Go 语言中没有“类”(class)的概念,也不支持“类”的继承等面向对象的概念。
Go 语言的结构体与“类”都是复合结构体(类似C语言),但 Go 语言中结构体的内嵌配合接口比面向对象具有更高的扩展性和灵活性。
Go 语言不仅认为结构体能拥有方法,且每种自定义类型也可以拥有自己的方法。
Go 语言结构体定义及实例化
Go 语言结构体格式定义(类似C语言)如下:
1 | type 类型名 struct { |
结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存。Go 语言实例化结构体方式有如下几种:
1 | type Employee struct { |
Go 语言(结构体)类型行为(方法)定义
类型行为(方法)是函数的特殊版本,它需要有名字,不能被当作值来看待,最重要的是,它必须隶属于某个自定义的数据类型,且不能是接口类型或接口的指针类型。方法所属的类型会通过其声明中的接收者(如下例e Employee
,其中必须包含确切的名称和类型字面量)声明体现出来,定义方式有如下:
1 | 1) func (e Employee) String() string { |
特别说明的是,第一种定义方式【值方法】在实例对应方法被调用时,实例的成员会进行值复制,通常为了避免内存拷贝我们使用第二种方式【指针方法】。一个自定义数据类型的方法集合中仅会包含它的所有值方法,而该类型的指针类型的方法集合却囊括了前者的所有方法,包括所有值方法和所有指针方法。
在 Go 语言中,我们可以通过为一个类型编写名为String的方法(没有任何参数声明,但需要有一个string类型的结果声明),来自定义该类型的字符串表示形式。当我们调用fmt.Printf函数时,使用占位符%s和e值本身就可以打印出后者的字符串表示形式,而无需显式地调用它的String方法。
1 | func main() { |
一个数据类型关联的所有方法,共同组成了该类型的方法集合。同一个方法集合中的方法不能出现重名,而且如果它们所属的是一个结构体类型,那么它们的名称与该类型中任何字段的名称也不能重复。
我们可以把结构体类型中的一个字段看作是它的一个属性(数据),再把隶属于它的一个方法看作是附加在其中数据之上的一个能力(操作)。将属性及其能力(数据及其操作)封装在一起,是面向对象编程(object-oriented programming)的一个主要原则。从这方面看,Go 语言其实是支持面向对象编程的,但它选择摒弃了一些在实际运用过程中容易引起程序开发者困惑的特性和规则。
Go 语言结构体内嵌
Go 语言结构体可以包含一个或多个匿名(内嵌)字段,这些字段没有显式的名字,只有必须的字段类型,此时该字段的类型既是类型也是名称。
匿名字段本身可以是一个结构体类型,即结构体可以包含内嵌结构体。实际上,嵌入字段的方法集合会被无条件地合并进被嵌入类型的方法集合中,这个粗略地可以和面向对象语言中的继承概念做比较,随后将会看到它被用来模拟类似继承的行为。
1 | package main |
如上例示,当我们使用fmt.Printf函数和%s占位符试图打印Department的字符串表示形式——相当于调用Department的String方法时,虽然我们还没为Department类型编写String方法,系统会 将嵌入字段Employee的String方法会被当做Department的方法调用。当我们也为Department类型编写一个String方法后,嵌入字段Employee的String方法就会被“屏蔽”掉(注意只要方法名称相同,无论这两个方法的签名是否一致,被嵌入类型的方法都会“屏蔽”掉嵌入字段的同名方法)。
类似的,由于我们同样可以像访问被嵌入类型的字段那样,直接访问嵌入字段的字段,所以 如果这两个结构体类型里存在同名的字段,那么嵌入字段中的那个字段也会被“屏蔽”。同时,因为嵌入字段的字段和方法都可以“嫁接”到被嵌入类型上,所以 即使在两个同名的成员一个是字段、另一个是方法的情况下,这种“屏蔽”现象依然会存在(即使被屏蔽了,我们仍然可以通过链式的选择表达式选择到嵌入字段的字段或方法)。
顺带提一下“多层嵌入”的问题,即 嵌入字段本身也有嵌入字段的情况,在这种情况下,“屏蔽”现象会以嵌入的层级为依据,嵌入层级越深的字段或方法越可能被“屏蔽”(原理雷同)。
思考问题: Go 语言是用嵌入字段实现了继承吗?
首先,Go 语言中根本没有继承的概念,它所做的是通过嵌入字段的方式实现了类型之间的组合。
简单来说,面向对象编程中的继承就是通过牺牲一定的代码简洁性来换取可扩展性,而且这种可扩展性是通过侵入的方式来实现的。Go 语言类型之间的组合采用的是非声明的方式,我们不需要显式地声明某个类型实现了某个接口,或者一个类型继承了另一个类型。同时,类型组合也是非侵入式的——不会破坏类型的封装或加重类型之间的耦合,我们只需把类型当做字段嵌入进来,然后坐享其成地使用嵌入字段所拥有的一切。如果嵌入字段有哪里不合心意,我们还可以用“包装”或“屏蔽”的方式去调整和优化。再者,组合要比继承更加简洁和清晰,Go 语言可以轻而易举地通过嵌入多个字段来实现功能强大的类型,却不会有多重继承那样复杂的层次结构和可观的管理成本。
此外,Go 语言中接口类型之间也可以组合,且更加常见,我们常常以此来扩展接口定义的行为或者标记接口的特征。
总的来说,Go 语言虽然支持面向对象编程,但是根本就没有“继承”这个概念。嵌入字段是实现类型间组合的一种方式,但对继承“雨女无瓜”。