Golang笔记-浅谈interface
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.
前言
class
和 interface
在高级语言中是很重要的概念。 class
是对模型的定义和封装, interface
则是对行为的抽象和封装。Go语言虽然没有 class
,但是有 struct
和 interface
,以另一种方式实现同样的效果。
本文将谈一谈Go语言这与别不同的 interface
的基本概念和一些需要注意的地方。
声明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
行为,实现了接口继承的效果。
实现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) }
上面这段代码,声明了一个名为 Sparrow
的 struct
,下面声明了两个方法。不过这个方法的声明行为可能略微有点奇怪。
比如 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
首先包含: Fly
和 Twitter
; Sparrow
包含 Walk
。
其次,当我们拥有一个指针类型的时候,因为有了这个变量的地址,我们得到这个具体的变量,所以一个指针类型的方法列表还可以包含其非指针类型作为接收者的方法。在我们的例子中就是 *Sparrow
的方法列表为: Fly
、 Twitter
和 Walk
,所以 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
字面量也无法取址。
因此在使用接口时,我们要注意不同类型的方法列表,是否实现接口。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK