通过嵌入结构体来扩展类型
来看看 ColoredPoint 这个类型:
Unresolved include directive in modules/ROOT/pages/ch6/ch6-03.adoc - include::example$/ch6/coloredpoint/main.go[]
我们完全可以将 ColoredPoint 定义为一个有三个字段的 struct ,但是我们却将 Point 这个类型嵌入到 ColoredPoint 来提供 X 和 Y 这两个字段。像我们在4.4节中看到的那样,内嵌可以使我们在定义 ColoredPoint 时得到一种句法上的简写形式,并使其包含 Point 类型所具有的一切字段,然后再定义一些自己的。如果我们想要的话,我们可以直接认为通过嵌入的字段就是 ColoredPoint 自身的字段,而完全不需要在调用时指出 Point ,比如下面这样。
var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X) // "1"
cp.Point.Y = 2
fmt.Println(cp.Y) // "2"
对于 Point 中的方法我们也有类似的用法,我们可以把 ColoredPoint 类型当作接收器来调用 Point 里的方法,即使 ColoredPoint 里没有声明这些方法:
red := color.RGBA{255, 0, 0, 255}
blue := color.RGBA{0, 0, 255, 255}
var p = ColoredPoint{Point{1, 1}, red}
var q = ColoredPoint{Point{5, 4}, blue}
fmt.Println(p.Distance(q.Point)) // "5"
p.ScaleBy(2)
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) // "10"
Point 类的方法也被引入了 ColoredPoint 。用这种方式,内嵌可以使我们定义字段特别多的复杂类型,我们可以将字段先按小类型分组,然后定义小类型的方法,之后再把它们组合起来。
读者如果对基于类来实现面向对象的语言比较熟悉的话,可能会倾向于将 Point 看作一个基类,而 ColoredPoint 看作其子类或者继承类,或者将 ColoredPoint 看作 "is a" Point 类型。但这是错误的理解。请注意上面例子中对 Distance 方法的调用。Distance 有一个参数是 Point 类型,但 q 并不是一个 Point 类,所以尽管 q 有着 Point 这个内嵌类型,我们也必须要显式地选择它。尝试直接传 q 的话你会看到下面这样的错误:
p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point
一个 ColoredPoint 并不是一个 Point ,但他"has a"Point,并且它有从 Point 类里引入的 Distance 和 ScaleBy 方法。如果你喜欢从实现的角度来考虑问题,内嵌字段会指导编译器去生成额外的包装方法来委托已经声明好的方法,和下面的形式是等价的:
func (p ColoredPoint) Distance(q Point) float64 {
return p.Point.Distance(q)
}
func (p *ColoredPoint) ScaleBy(factor float64) {
p.Point.ScaleBy(factor)
}
当 Point.Distance 被第一个包装方法调用时,它的接收器值是 p.Point ,而不是 p ,当然了,在 Point 类的方法里,你是访问不到 ColoredPoint 的任何字段的。
在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中(译注:访问需要通过该指针指向的对象去取)。添加这一层间接关系让我们可以共享通用的结构并动态地改变对象之间的关系。下面这个 ColoredPoint 的声明内嵌了一个 *Point
的指针。
Unresolved include directive in modules/ROOT/pages/ch6/ch6-03.adoc - include::example$/ch6/coloredpoint/main.go[]
一个 struct 类型也可能会有多个匿名字段。我们将 ColoredPoint 定义为下面这样:
type ColoredPoint struct {
Point
color.RGBA
}
然后这种类型的值便会拥有 Point 和 RGBA 类型的所有方法,以及直接定义在 ColoredPoint 中的方法。当编译器解析一个选择器到方法时,比如 p.ScaleBy ,它会首先去找直接定义在这个类型里的 ScaleBy 方法,然后找被 ColoredPoint 的内嵌字段们引入的方法,然后去找 Point 和 RGBA 的内嵌字段引入的方法,然后一直递归向下找。如果选择器有二义性的话编译器会报错,比如你在同一级里有两个同名的方法。
方法只能在命名类型(像 Point)或者指向类型的指针上定义,但是多亏了内嵌,有些时候我们给 匿名 struct 类型 来定义方法也有了手段。
下面是一个小trick。这个例子展示了简单的 cache,其使用两个包级别的变量来实现,一个 mutex 互斥锁(§9.2)和它所操作的 cache:
var (
mu sync.Mutex // guards mapping
mapping = make(map[string]string)
)
func Lookup(key string) string {
mu.Lock()
v := mapping[key]
mu.Unlock()
return v
}
下面这个版本在功能上是一致的,但将两个包级别的变量放在了 cache 这个 struct 一组内:
var cache = struct {
sync.Mutex
mapping map[string]string
}{
mapping: make(map[string]string),
} // 定义并初始化匿名结构体
func Lookup(key string) string {
cache.Lock()
v := cache.mapping[key]
cache.Unlock()
return v
}
我们给新的变量起了一个更具表达性的名字:cache 。因为 sync.Mutex 字段也被嵌入到了这个 struct 里,其 Lock 和 Unlock 方法也就都被引入到了这个匿名结构中了,这让我们能够以一个简单明了的语法来对其进行加锁解锁操作。