Files
the-way-to-go_ZH_CN/eBook/11.12.md
2015-11-02 17:17:45 +08:00

7.5 KiB
Raw Blame History

11.12 接口与动态类型

11.12.1 Go的动态类型

在经典的 OO 语言(像 C++Java 和 C#)中数据和方法被封装为 类的概念:类包含它们全部,并且不能分开。

Go 没有类:数据(结构体或更一般的类型)和方法是正交关系,一种松耦合。

Go 中的接口跟 Java/C# 类似:都是必须提供一个指定方法集的实现。但是更灵活通用:任何提供了接口方法实现代码的类型都隐式地实现了该接口,而不用显式地声明。

和其它语言相比Go 是唯一结合了接口值,静态类型检查(是否该类型实现了某个接口),运行时动态转换的语言,并且不需要显式地声明类型是否满足某个接口。该特性允许我们定义和使用新接口,而不用改变已有的代码。

以一个(或多个)接口类型作为参数的函数,可以被实现了该接口的类型的实例调用。实现了某个接口的类型可以被传给任何以此接口为参数的函数

类似于 Python 和 Ruby 这类动态语言中的 动态类型duck typing;这意味着对象可以根据提供的方法被处理(例如,作为参数传递给函数),而忽略它们的实际类型:它们能做什么比它们是什么更重要。

这在程序 duck_dance.go 中得以阐明,函数 DuckDance 接受一个 IDuck 的接口类型变量。仅当 DuckDance 是被实现了 IDuck 接口的类型调用时程序才能通过编译。

示例 11.16 duck_dance.go

package main

import "fmt"

type IDuck interface {
	Quack()
	Walk()
}

func DuckDance(duck IDuck) {
	for i := 1; i <= 3; i++ {
		duck.Quack()
		duck.Walk()
	}
}

type Bird struct {
	// ...
}

func (b *Bird) Quack() {
	fmt.Println("I am quacking!")
}

func (b *Bird) Walk()  {
	fmt.Println("I am walking!")
}

func main() {
	b := new(Bird)
	DuckDance(b)
}

/** Output:
	I am quacking!
	I am walking!
	I am quacking!
	I am walking!
	I am quacking!
	I am walking!
*/

如果 Bird 没有实现 Walk()(把它注释掉),会得到一个编译错误:

cannot use b (type *Bird) as type IDuck in function argument:
*Bird does not implement IDuck (missing Walk method)

如果对 cat 调用函数 DuckDance()Go 会提示编译错误,但是 Python 和 Ruby 会以运行时错误结束。

11.12.2 动态方法调用

像 PythonRuby 这类语言,动态类型是延迟绑定的(在运行时进行):方法只是用参数和变量简单地调用,然后在运行时才解析(它们很可能有像 responds_to 这样的方法来检查对象是否可以响应某个方法,但是这也意味着更大的编码量和更多的测试)

Go 的实现与此相反,通常需要编译器静态检查的支持:当变量被赋值给一个接口类型的变量时,编译器会检查其是否实现了该接口的所有函数。如果方法调用作用于像 interface{} 这样的“泛型”上,你可以通过类型断言(参见 11.3 节)来检查变量是否实现了相应接口。

例如,假设你用不同的类型表示 XML 输出流中的不同实体。然后我们为 XML 定义一个如下的“写”接口(甚至可以把它定义为私有接口):

type xmlWriter interface {
	WriteXML(w io.Writer) error
}

现在我们可以实现适用于流类型的任何变量的 StreamXML 函数,并用类型断言检查传入的变量是否实现了该接口;如果没有,我们就调用内建的 encodeToXML 来完成相应工作:

// Exported XML streaming function.
func StreamXML(v interface{}, w io.Writer) error {
	if xw, ok := v.(xmlWriter); ok {
		// Its an  xmlWriter, use method of asserted type.
		return xw.WriteXML(w)
	}
	// No implementation, so we have to use our own function (with perhaps reflection):
	return encodeToXML(v, w)
}

// Internal XML encoding function.
func encodeToXML(v interface{}, w io.Writer) error {
	// ...
}

Go 在这里用了和 gob 相同的机制:定义了两个接口 GobEncoderGobDecoder。这样就允许类型自己实现从流编解码的方式;如果没有就使用标准的反射方式。

因此 Go 提供了动态语言的优点,却没有其他动态语言在运行时可能发生错误的缺点。

对于动态语言非常重要的单元测试来说,这样即可以减少单元测试的部分需求,又可以发挥相当大的作用。

Go 的接口提高了代码的分离度,改善了代码的复用性,使得代码开发过程中的设计模式更容易实现。用 Go 接口还能实现 依赖注入模式

11.12.3 接口的提取

提取接口 是非常有用的设计模式,可以减少需要的类型和方法数量,而且不需要像传统的基于类的 OO 语言那样维护整个的类层次结构。

Go 接口可以让开发者找出自己写的程序中的类型。假设有一些拥有共同行为的对象,并且开发者想要抽象出这些行为,这时就可以创建一个接口来使用。 我们来扩展 11.1 节的示例 11.2 interfaces_poly.go假设我们发现我们需要一个新的接口 TopologicalGenus,用来给 shape 排序(这里简单地实现为返回 int。我们需要做的是给想要满足接口的类型实现 Rank() 方法:

示例 11.17 multi_interfaces_poly.go

//multi_interfaces_poly.go
package main

import "fmt"

type Shaper interface {
	Area() float32
}

type TopologicalGenus interface {
	Rank() int
}

type Square struct {
	side float32
}

func (sq *Square) Area() float32 {
	return sq.side * sq.side
}

func (sq *Square) Rank() int {
	return 1
}

type Rectangle struct {
	length, width float32
}

func (r Rectangle) Area() float32 {
	return r.length * r.width
}

func (r Rectangle) Rank() int {
	return 2
}

func main() {
	r := Rectangle{5, 3} // Area() of Rectangle needs a value
	q := &Square{5}      // Area() of Square needs a pointer
	shapes := []Shaper{r, q}
	fmt.Println("Looping through shapes for area ...")
	for n, _ := range shapes {
		fmt.Println("Shape details: ", shapes[n])
		fmt.Println("Area of this shape is: ", shapes[n].Area())
	}
	topgen := []TopologicalGenus{r, q}
	fmt.Println("Looping through topgen for rank ...")
	for n, _ := range topgen {
		fmt.Println("Shape details: ", topgen[n])
		fmt.Println("Topological Genus of this shape is: ", topgen[n].Rank())
	}
}
/* Output:
Looping through shapes for area ...
Shape details:  {5 3}
Area of this shape is:  15
Shape details:  &{5}
Area of this shape is:  25
Looping through topgen for rank ...
Shape details:  {5 3}
Topological Genus of this shape is:  2
Shape details:  &{5}
Topological Genus of this shape is:  1
*/

所以你不用提前设计出所有的接口;整个设计可以持续演进,而不用废弃之前的决定。当类型要实现一个接口,它本身不用改变,你只需要在这个类型上实现新的方法。

11.12.4 显式地指明类型实现了某个接口

如果你希望满足某个接口的类型显式地声明它们实现了这个接口,你可以向接口的方法集中添加一个具有描述性名字的方法。例如:

type Fooer interface {
	Foo()
	ImplementsFooer()
}

类型 Bar 必须实现 ImplementsFooer 方法来满足 Footer 接口,以清楚地记录这个事实。

type Bar struct{}
func (b Bar) ImplementsFooer() {} func (b Bar) Foo() {}

大部分代码并不使用这样的约束,因为它限制了接口的实用性。

但是有些时候,这样的约束在大量相似的接口中被用来解决歧义。

11.12.5 空接口和函数重载