Key Concepts
Go语言的面向对像跟其它语言比如C++, Java的不同在于它不支持继承。当面向对像程序第一流行时,继承被吹捧为它最大的一个特点。但是现在,经过几十年的经验,这已经变成了显著的缺点,特别是维护一个大型的系统来说。取而代之的是聚合和继承一起使用。而Go支持的是聚合(aggregation)和嵌入(embedding). 为了区分聚合和嵌入的不同,我们看以下例子.
type ColoredPoint struct {
color.Color // Anonymous field (embedding)
x, y int // Named fields (aggregation)
}
在这里,color.Color是一个类型,它来自于image/color package,x和y分别是int. 在Go语言的专业术语中,color.Color, x, y都是ColoredPoint struct的字段(field)。 color.Color字段是匿名的(由于它没有变量名), 因此是一个嵌入的字段。x, y字段是命名的聚合字段。如果我们创建了一个ColoredPoint的值(e.g., point := ColoredPoint{}), 它的字段可以通过point.Color, point.x和point.y访问。需要注意的是,当访问的一个字段来自于另一个包,我们仅需要用到这个名字的最后一部分,Color而不是color.Color
专业术语"class", "object"和"instance"只存在于继承结构的面向对像语言中,在Go语言中,我们应该避免使用,而应该使用"types"和“values".
没有继承就没有虚函数。对于此,Go支持类型安全的鸭子类型。在Go中,参数可以被指定为具休一的类型(e.g., int, string, *os.File, MyType)或者一个接口——只要传递的值提供的方法满足接口。对于接口类型的参数,我们可以传递任何的值——只要它的方法满足于接口。举例来说,如果我们有一个值,它提供了一个方法 Write([]byte) (int, error) 我们就可以将这个值传递给任何有要求参数为io.Writer的函数,而不用管这个函数的实际类型是什么。
使用继承的一个优势是一些方法只需要在基类上实现,而所以有的子类都会受益。Go为实现这样的能力,提供了两种方法。一种是使用嵌入(embedding). 如果我们嵌入的是一个类型,那么被嵌入类型的方法也对包含它的类型有效。另一个解决办法是为所有的类型提供方法的单独版本。
Go的面向对像另一个不同寻常是接口,值,和方法都保持独立。接口用来提定方法的签名,structs用来聚合和嵌入值,方法用来提供自定义类型(通常为struct)的操作. 在自定义类型和任何特定的接口之间没有明确的联系——但如果一个类型的方法实现了一个或者多个接口。那么这种类型的值可以被用于要求这些接口的地方。当然,每一种类型都满足于空接口(interface{}),所以任何值都可以被于用要求空接口的地方。
我们可以认为在Go中的接口与值是is-a的关系,接口中只是纯粹的方法签名。所以当一个值实现了io.Reader接口(i.e.,它有一个方法的签名为Read([]byte) (int,error)), 而它不是一个reader,可能为一个文件,buffer,或者自定义类型,但只要它有一个Read()方法就可以了,如图6.1所示。而struct表示是的has-a的关系,我们可能通过聚合或者嵌入特定的类型,构成我们的自定义类型。
虽然不可能为内置类型添加方法,但可以很容易的创建基于内置类型的自定义类型,并且给它添加方法。这种类型的值可以使用我们提供的方法,也可以使用底替的内置类型提供的函数,方法和操作符。比如,如果我们有一个类型Integer int, 我们可以使用int类型的 + 操作符。一旦我们有一个自定义类型,我们就可以添加自定义的方法——比如 func(i Interger) Double() Integer {return i * 2}.
不仅创建基于内置类型的自定义类型非常简单,它们使用时也非常高效。 在内置与自定义类型之间转换是不需要花费运行时间的,因为这种转换是在编译时完成的。鉴于此,将一个内置类型“升级”为自定义类型是为了使用它的自定义方法,而将一个自定义类型“降级“为内置类型是当我们想要将它们传递到的函数,它的参数为内置类型。我们在之前的看到过将一个[]string 转换为FoldedString, 同时我们在本章中看到,如何“降级” Count type.