Golang笔记-浅谈interface

tinywell · · 13096 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

前言

classinterface在高级语言中是很重要的概念。class是对模型的定义和封装,interface则是对行为的抽象和封装。Go语言虽然没有class,但是有structinterface,以另一种方式实现同样的效果。

本文将谈一谈Go语言这与别不同的interface的基本概念和一些需要注意的地方。

声明interface

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

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

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

type Chicken interface {
    Bird
    Walk()
}

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

实现interface

在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 {
    // ...
    return fmt.Sprintf("%s,jojojo", s.name)
}

上面这段代码,声明了一个名为Sparrowstruct,下面声明了两个方法。不过这个方法的声明行为可能略微有点奇怪。

比如func (s *Sparrow) Fly(hign int) bool中,func关键字用于声明方法和函数,后面方法Fly以及参数和返回值。但是在func关键字和方法名Fly中间还有s *Sparraw的声明,这个声明在Go中称之为接受者声明,其中s代表这个方法的接收者,*Sparrow代表这个接收者的类型。

接收者的类型可以为一个数据类型的指针类型,也可以是数据类型本身,比如我们针对Sparrow再实现一个方法:

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

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

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

使用interface

利用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{}
    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
Sparrow Twitter
Sparrow Fly
BirdAnimation of *main.Sparrow
Sparrow Twitter
Sparrow Fly

深入一步interface

关于空interface

先看一段代码,猜猜会输出什么。

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

关于方法列表

再看一段代码:

func ChickenAnimation(chicken Chicken) {
    fmt.Printf("ChickenAnimation of %T\n", chicken)
    chicken.Walk()
    chicken.Twitter()
}

func main() {
    var chicken Chicken
    sparrow2 := Sparrow{}
    chicken = sparrow2
    ChickenAnimation(chicken)
}

其运行结果如下:

➜  go run main.go
# command-line-arguments
./main.go:70:10: cannot use sparrow2 (type Sparrow) as type Chicken in assignment:
        Sparrow does not implement Chicken (Fly method has pointer receiver)

编译器编译报错,它说Sparrow并没有实现Chicken接口,因为Fly方法的接受者是指针接收者,而我们给的是Sparrow

我们将程序做一点小小的调整就可以了,将第10行代码修改为:

chicken = &sparrow2

也许你会问:"Chicken接口的Walk方法的接收者是非指针的Sparrow,我们把*Sparrow赋值给Chicken接口变量为什么可以通过?"。

这里就要讲到方法列表的概念。

首先,一个指针类型的方法列表必然包含所有接收者为指针接收者的方法,同理非指针类型的方法列表也包含所有接收者为非指针类型的方法。在我们例子中*Sparrow首先包含:FlyTwitterSparrow包含Walk

其次,当我们拥有一个指针类型的时候,因为有了这个变量的地址,我们得到这个具体的变量,所以一个指针类型的方法列表还可以包含其非指针类型作为接收者的方法。在我们的例子中就是*Sparrow的方法列表为:FlyTwitterWalk,所以chicken = &sparrow2可以通过。

但是一个非指针类型却并不总是能取到它的地址,从而获取它接收者为指针接收者的方法。所以非指针类型的方法列表中只有接收者为非指针类型的方法。如果它的方法列表不能完全覆盖这个接口,是不算实现了这个接口的。

举个简单的例子:

type TestInt int

func main() {
  &TestInt(7)
}

编译报错,无法取址:

➜  go run main.go
# command-line-arguments
./main.go:77:2: cannot take the address of TestInt(7)
./main.go:77:2: &TestInt(7) evaluated but not used

又或者:

func main() {
    sparrow4 := Sparrow{}
    sparrow4.Twitter()
}

这样可以正常运行,但是稍微改改:

func main() {
    Sparrow{}.Twitter()
}

则编译报错:

➜  go run main.go
# command-line-arguments
./main.go:80:11: cannot call pointer method on Sparrow literal
./main.go:80:11: cannot take the address of Sparrow literal

字面量也无法取址。
因此在使用接口时,我们要注意不同类型的方法列表,是否实现接口。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:tinywell

查看原文:Golang笔记-浅谈interface

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

13096 次点击  ∙  1 赞  
加入收藏 微博
2 回复  |  直到 2019-06-14 16:08:26
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传