前言
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
字面量也无法取址。
因此在使用接口时,我们要注意不同类型的方法列表,是否实现接口。
有疑问加站长微信联系(非本文作者)