Golang笔记-浅谈interface

前言

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容