Golang入门——动态类型
- Go的动态类型
- 动态方法调用
- 接口的提取
- 显式地指明类型实现了某个接口
- 空接口和函数重载
- 接口的继承
- 代码下载
Go的动态类型
在面向对象语言(C++、Java等 )中数据和方法被封装为 类 的概念。而Go中没有类,其数据和方法是一种松耦合的正交关系。
Go中的接口和 Java / C# 类似:都是必须提供一个指定方法集实现。但是更加灵活通用:任何提供了接口方法实现代码的类型都隐式地实现了该端口,而不用显式地声明。
接收一个或多个接口类型作为参数的函数,其实参可以是任何实现了该接口的类型的变量。实现了某个接口的类型可以被传给任何以此接口为参数的函数。
type IDuck interface {
Quack()
Walk()
}
func DuckDancce(duck IDuck){
for i := 1 ; i <= 3 ; i++{
duck.Quack()
duck.Walk()
}
}
type Bird struct{
//Anything
}
func (b *Bird) Quack(){
fmt.Println("I'm quacking")
}
func (b *Bird) Walk(){
fmt.Println("I'm walking")
}
func main(){
b := new(Bird)
DuckDancce(b)
}
输出结果:
I'm quacking
I'm walking
I'm quacking
I'm walking
I'm quacking
I'm walking
如果 Bird 没有实现 Walk( ) (把他注视掉),则会出现编译错误。如果对 cat 调用 DuckDancce( ), 也会出现编译错误,而Python和Ruby会以运行时错误结束。
动态方法调用
Go需要编译器静态检查的支持:当变量被赋值给一个接口类型的变量时,编译器会检查是否实现了对该接口的所有函数。如果方法调用作用于像 interface{ } 这样的 “ 泛型 ”上,可以通过类型断言来检查变量是否实现了相应接口。
例如,你用不同的类型表示XML输出流中的不同实体。然后我们为XML定义一个如下的 “写” 接口:
type xmlWriter interface{
WriteXML(w io.Writer) error
}
func StreamXML(v interface{}, w io.Writer) error{
if xw,ok := v.(xmlWriter); ok {
return xw.WriteXML(w)
}
return encodeToXML(v,w)
}
func encodeToXML(v interface{},w io.Writer) error{
// Anything
return nil
}
Go在这里用了和 gob 相同的机制:定义了两个接口:GobEncoder 和GobDecoder。这样就允许类型自己实现流编解码的具体方式;如果没有实现就是用标准的反射方式。
因此Go提供了动态语言的优点,却没有其他动态语言在运行时可能发生错误的缺点。
Go 的接口提高了代码的分离度,改善了代码的复用性,使得代码开发过程中设计模式容易实现。用Go 接口还能实现 依赖注入模式。
接口的提取
提取接口时非常有用的设计模式,可以减少需要的类型和方法数量,而且不需要像传统的基于类的面向对象语言那样维护整个的类层次结构。
Go接口可以让开发者找出自己写的程序中的类型。假设有一些拥有共同行为的对象,并且开发者想要抽象出这些行为,这时就可以创建一个接口来使用。
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}
q := &Square{5}
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 togpen for rank...")
for n := range topgen {
fmt.Println("Shapes details:",topgen[n])
fmt.Println("Topological Genus of this shape is:",topgen[n].Rank())
}
}
输出结果:
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 togpen for rank...
Shapes details: {5 3}
Topological Genus of this shape is: 2
Shapes details: &{5}
Topological Genus of this shape is: 1
所以不需要提前设计出所有的接口;整个设计可以持续演进,而不用废弃之前的决定。类型要实现某个接口,它本身不用改变,只需要在这个类型上实现新的方法即可。
显式地指明类型实现了某个接口
显式地指明类型实现了某个接口,可以向接口的方法集中添加一个具有描述性名字的方法:
type Fooer interface {
Foo()
ImplementsFooer()
}
type Bar struct {}
func (b Bar) ImplementsFooer(){}
func (b Bar) Foo(){}
类型 Bar 必须实现 ImplementsFooer 方法来满足 Fooer 接口,以清楚地记录这个事实。
大部分代码并不使用这样的约束,因为他限制了接口的实用性。但是有些时候,这样的约束在大量相似的接口中被用来解决歧义。
空接口和函数重载
Go中函数的重载是不被允许的。在 Go 语言中函数重载可以用可变参数 …T 作为函数的最后一个参数来实现。如果把 T 换为空接口,那么可以知道任何类型的变量都是满足空接口类型的,这样就允许传奇任何数量任何类型的参数给函数,即重载的实际含义。
函数 fmt.Printf( ) 就是这样做的
fmt.Printf(format string,a ...interface{}) (n int, errno error)
这个函数通过枚举 slice 类型的实参动态确定所有参数的类型。并且查看每个类型是否实现了 String( ) 方法。
接口的继承
当一个类型包含另一个类型的指针时,这个类型就可以使用所有的接口方法。
type Task struct{
Command string
*log.Logger
}
这个类型的工厂方法就这样:
func NewTask(command string, logger *log.Logger) *Task{
return &Task{command,logger}
}
当 log.Logger 实现了 Log( ) 方法后, Task 的实例 task 就可以调用该方法:
task.Log()
类型可以通过继承多个接口来提供像 多重继承 一样的特性:
type ReadWrite struct {
*io.Reader
*io.Writer
}
代码下载
以上程序代码均已上传到至github ,有需要可直接进行下载