Go语言接口与动态类型
在经典的面向对象语言(像 C++,Java 和 C#)中数据和方法被封装为类的概念:类包含它们两者,并且不能剥离。 Go语言没有类:数据(结构体或更一般的类型)和方法是一种松耦合的正交关系。 Go语言中的接口跟 Java/C# 类似:都是必须提供一个指定方法集的实现。但是更加灵活通用:任何提供了接口方法实现代码的类型都隐式地实现了该接口,而不用显式地声明。 Go 的动态类型和其它语言相比,Go语言是唯一结合了接口值,静态类型检查(是否该类型实现了某个接口),运行时动态转换的语言,并且不需要显式地声明类型是否满足某个接口。该特性允许我们在不改变已有的代码的情况下定义和使用新接口。接收一个(或多个)接口类型作为参数的函数,可以被实现了该接口的类型实例调用。实现了某个接口的类型可以被传给任何以此接口为参数的函数。 类似于 Python 和 Ruby 这类动态语言中的动态类型(duck typing);这意味着对象可以根据提供的方法被处理(例如,作为参数传递给函数),而忽略它们的实际类型:它们能做什么比它们是什么更重要。 这在程序 duck_dance.go 中得以阐明,函数 DuckDance 接受一个 IDuck 接口类型变量。仅当 DuckDance 被实现了 IDuck 接口的类型调用时程序才能编译通过。 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) }输出:
I am quacking!
cannot use b (type *Bird) as type IDuck in function argument: 动态方法调用像 Python,Ruby 这类语言,动态类型是延迟绑定的(在运行时进行):方法只是用参数和变量简单地调用,然后在运行时才解析(它们很可能有像 responds_to 这样的方法来检查对象是否可以响应某个方法,但是这也意味着更大的编码量和更多的测试工作)。Go语言的实现与此相反,通常需要编译器静态检查的支持:当变量被赋值给一个接口类型的变量时,编译器会检查其是否实现了该接口的所有函数。如果方法调用作用于像 interface{} 这样的“泛型”上,可以通过类型断言来检查变量是否实现了相应接口。 例如,用不同的类型表示 XML 输出流中的不同实体。然后我们为 XML 定义一个如下的“写”接口(甚至可以把它定义为私有接口):
type xmlWriter interface {
// Exported XML streaming function. 因此 Go 提供了动态语言的优点,却没有其他动态语言在运行时可能发生错误的缺点。对于动态语言非常重要的单元测试来说,这样即可以减少单元测试的部分需求,又可以发挥相当大的作用。 Go 的接口提高了代码的分离度,改善了代码的复用性,使得代码开发过程中的设计模式更容易实现。用 Go 接口还能实现依赖注入模式 。 接口的提取提取接口是非常有用的设计模式,可以减少需要的类型和方法数量,而且不需要像传统的基于类的面向对象语言那样维护整个的类层次结构。Go 接口可以让开发者找出自己写的程序中的类型。假设有一些拥有共同行为的对象,并且开发者想要抽象出这些行为,这时就可以创建一个接口来使用。假设我们需要一个新的接口 TopologicalGenus,用来给 shape 排序(这里简单地实现为返回 int)。我们需要做的是给想要满足接口的类型实现 Rank() 方法: //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()) } }输出:
Looping through shapes for area ... 接口,它本身不用改变,只需要在这个类型上实现新的方法。 显式地指明类型实现了某个接口如果希望满足某个接口的类型显式地声明它们实现了这个接口,可以向接口的方法集中添加一个具有描述性名字的方法。例如:
type Fooer interface {
type Bar struct{} 空接口和函数重载在 Go语言中函数重载可以用可变参数 ...T 作为函数最后一个参数来实现。如果我们把 T 换为空接口,那么可以知道任何类型的变量都是满足 T (空接口) 类型的,这样就允许我们传递任何数量任何类型的参数给函数,即重载的实际含义。函数 fmt.Printf 就是这样做的: fmt.Printf(format string,a ...interface{}) (n int,errno error) 这个函数通过枚举 slice 类型的实参动态确定所有参数的类型。并查看每个类型是否实现了 String() 方法。接口的继承当一个类型包含(内嵌)另一个类型(实现了一个或多个接口)的指针时,这个类型就可以使用(另一个类型)所有的接口方法。例如:
type Task struct {
func NewTask(command string,logger *log.Logger) *Task { task.Log() 类型可以通过继承多个接口来提供像 多重继承一样的特性:
type ReaderWriter struct { 有用的接口可以在开发的过程中被归纳出来。添加新接口非常容易,因为已有的类型不用变动(仅仅需要实现新接口的方法)。已有的函数可以扩展为使用接口类型的约束性参数:通常只有函数签名需要改变。对比基于类的 OO 类型的语言在这种情况下则需要适应整个类层次结构的变化。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |