最近又在学习一门面向对象的语言:golang。这门静态语言和ruby和是完全不同风格的语言,但是其语法还算简单,加上一些简单易懂的框架,上手写一些业务的接口代码并没有什么难度。但是现在对interface的概念还是理解得不够透彻,这篇博客会记录一下一些学习过程的感想。

面向对象的特征

class和interface在高级语言中是很重要的概念。class是对模型的定义和封装,interface则是对行为的抽象和封装。因为以前都是写ruby(纯面向对象的语言),而且还是基于rails的框架,对面向对象的概念只有一些模糊的理解。所以在提及面向对象的特征时候,会顺便对ruby做一些分析。

  1. 数据封装:数据封装的概念很容易理解,就是所有的对象都有自己的属性或者成员,而不是依赖于全局或者其他的局部变量。
  2. 继承:直接继承父类的方法和属性。继承这特性在ruby中也比较容易实现,但是golang中没有类的概念,这门语言依靠struct,也可以实现继承特性。
  3. 多态性:多态性其实就是多重继承,即一个子类可以同时继承多个父类。在ruby中,广义上的多重继承是禁止的,但是ruby使用了mixin的设计模式,巧妙地实现了多重继承。golang中自然是使用了interface来实现多重继承,也是本文的重点。

接口声明与接口继承

type Birds interface {
    Twitter() string
    Fly(high int) bool
}

上面这段代码声明了一个名为Birds的接口类型(interface),这个接口包含两个行为Twitter和Fly。
Go语言里面,声明一个接口类型需要使用type关键字、接口类型名称、interface关键字和一组有{}括起来的方法声明,这些方法声明只有方法名、参数和返回值,不需要方法体。

Go语言没有继承的概念,也没有类的概念,那如果需要实现继承的效果怎么办?Go的方法是嵌入,以嵌入的方法来实现接口的继承。

type Chicken interface {
    Bird
    Walk()
}

上面这段代码中声明了一个新的接口类型Chicken,我们希望他能够共用Birds的行为,于是直接在Chicken的接口类型声明中,嵌入Birds接口类型,这样Chicken接口中就有了原属于Birds的Twitter和Fly这两个行为以及新增加的Walk行为,实现了接口继承的效果。

接口实现

在java中,通过类来实现接口。一个类需要在声明通过implements显示说明实现哪些接口,并在类的方法中实现所有的接口方法。Go语言没有类,也没有implements,如何来实现一个接口呢?这里就体现了Go与别不同的地方了。

首先,Go语言没有类但是有struct,通过struct来定义模型结构和方法。

其次,Go语言实现一个接口并不需要显示声明,而是只要你实现了接口中的所有方法就认为你实现了这个接口。这称之为Duck typing。

如果它走起步来像鸭子,并且叫声像鸭子, 那个它一定是一只鸭子。

说道这里,就需要介绍下struct如何实现方法。

type Sparrow struct {
    name string
}

func (s *Sparrow) Fly(hign int) bool {
    return true
}

func (s *Sparrow) Twitter() string {
    fmt.Printf("%s,jojojo", s.name)
}

上面这段代码,声明了一个名为Sparrow的struct,下面声明了两个方法。不过这个方法的声明行为可能略微有点奇怪。
比如func (s *Sparrow) Fly(hign int) bool中,func关键字用于声明方法和函数,后面方法Fly以及参数和返回值。但是在func关键字和方法名Fly中间还有s *Sparraw的声明,这个声明在Go中称之为接收者声明,其中s代表这个方法的接收者,*Sparrow代表这个接收者的类型。
接收者的类型可以为一个数据类型的指针类型,也可以是数据类型本身,比如我们针对Sparrow再实现一个方法:

func (s Sparrow) Walk() {
    // ...
}

接收者为数据类型(值)的方法称为值方法,接收者为指针类型的方法称之为指针方法。

这种非侵入式的接口实现方式非常的方便和灵活,不用去管理各种接口依赖,对开发人员来说也更简洁。

接口使用

利用struct去实现接口之后,我们就可以用这个struct作为接口参数,使用那些接收接口参数的方法完成我们的功能。这也是面向接口编程的方式,我们的功能依据接口来实现,而不用关心实现接口的是什么,这样大大提供了功能的通用性可扩展性。

func BirdAnimation(bird Birds, high int) {
    fmt.Printf("BirdAnimation of %T\n", bird)
    bird.Twitter()
    bird.Fly(high)
}

func main() {
    var bird Birds
    sparrow := &Sparrow{name: 'test' }
    bird = sparrow
    BirdAnimation(bird, 1000)
    // 或者将sparrow直接作为参数
    BirdAnimation(sparrow, 1000)
}

上面这段代码中,我们声明了一个Birds接口类型的变量bird,由于*Sparrow实现了Birds接口的所有方法,所以我们可以将*Sparrow类型的变量sparrow赋值给bird。或者直接将sparrow作为参数调用BirdAnimation,运行结果如下:

➜  go run main.go
BirdAnimation of *main.Sparrow
test, jojojo
BirdAnimation of *main.Sparrow
test,jojojo

我们可以观察一下上面例子,我们都是使用的指针接收者方法。开阔一下思路,现在将他们全部换成是值接收者方法:

func (s Sparrow) Fly(hign int) bool {
    return true
}

func (s Sparrow) Twitter() string {
    fmt.Printf("%s,jojojo", s.name)
}

神奇的是,这两次的打印结果是一样的,来看一下go的编译器为我们做了什么隐式地操作。其实是很简单,从我们打印的type来看,都是“BirdAnimation of *main.Sparrow”,说明我们的入参数都是*Sparrow类型。当我们调用方法的时候,指针被解引用为值,这样就可以符合值接收者的需求。

(*bird).Twitter()
(*bird).Fly(high)

空接口

先看看一段代码:

func NilInterfaceTest(chicken Chicken) {
    if chicken == nil {
        fmt.Println("Sorry,It’s Nil")
    } else {
        fmt.Println("Animation Start!")
        ChickenAnimation(chicken)
    }
}

func main() {
  var sparrow3 *Sparrow
  NilInterfaceTest(sparrow3)
}

我们声明了一个*Sparrow的变量sparrow3,但是我们并没有对其进行初始化,是一个nil值,然后我们直接将它作为参数调用NilInterfaceTest(),我们预期的结果是希望NilInterfaceTest方法检测出nil值,避免出错。然而实际结果是这样的:

➜  go run main.go
Animation Start!
ChickenAnimation of *main.Sparrow
panic: value method main.Sparrow.Walk called using nil *Sparrow pointer

goroutine 1 [running]:
...

NilInterfaceTest方法并没有检测到我们传的是一个nil的sparrow,正常去使用最终导致了程序panic。

也许这里很让人迷惑,其实这里应该认识到虽然我们可以将实现了接口所有方法的接收者当做接口来使用,但是两者并不是完全等同。在Go语言中,interface的底层结构其实是比较复杂的,简要来说,一个interface结构包含两部分:1.这个接口值的类型;2.指向这个接口值的指针。我们稍微在NilInterfaceTest代码中加点东西看看:

func NilInterfaceTest(chicken Chicken) {
    if chicken == nil {
        fmt.Println("Sorry,It’s Nil")
    } else {
        fmt.Println("Animation Start!")
        fmt.Printf("type:%v,value:%v\n", reflect.TypeOf(chicken), reflect.ValueOf(chicken))
        ChickenAnimation(chicken)
    }
}

我们增加了第6行的代码,将bird变量的类型和值分别输出,得到结果如下:

➜  go run main.go
Animation Start!
type:*main.Sparrow,value:<nil>
ChickenAnimation of *main.Sparrow
panic: value method main.Sparrow.Walk called using nil *Sparrow pointer
...

我们可以看到bird的type为main.Sparrow,而value为nil。也就是说,我们将一个nil的Sparrow赋值给bird后,这个bird的type部分就已经有值了,只不过他的value部分是nil,所以bird并不是nil。