38

Golang笔记-浅谈interface

 5 years ago
source link: https://studygolang.com/articles/14318?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

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

字面量也无法取址。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK