4.3 接口
接口即约定,通过interface关键字定义了接口以后,凡是满足定义的都被认定为该接口的实现。这是隐式实现方式,与Java通过implements关键字显式实现是完全不同的。
关于隐式实现,有一个非常形象的说明:小黄鸭(小孩子洗澡时玩的小玩具)是不是鸭子类型?Go语言中认为只要像鸭子,有鸭子的嘴、鸭子的脚、鸭子的身体,那么就是鸭子。也就是说只要定义了一个接口,且某个类型完全满足这个接口的定义,那么这个类型就实现了这个接口,不再需要单独使用某个关键字去说明。
注意
Go语言的这种接口隐式实现方式允许在具体类型已经存在的情况下再去定义接口,这样也不会破坏原来的定义。
接口定义了需要被实现的一组函数方法的抽象集合,如果要实现某个接口就必须实现该接口的所有方法。
先来看一下接口使用的示例代码,如下:
book/ch04/4.3/base/main.go
1. package main
2.
3. import (
4. "fmt"
5. "math"
6. )
7.
8. type ShapeDesc interface {
9. Area() float64
10. Perimeter() float64
11. }
12.
13. type rectangle struct {
14. H,W float64
15. }
16.
17. type circle struct {
18. R float64
19. }
20.
21. func (r rectangle) Area() float64 {
22. return r.H * r.W
23. }
24. func (r rectangle) Perimeter() float64 {
25. return 2*(r.H+r.W)
26. }
27.
28. func (c circle) Area() float64 {
29. return c.R*c.R*math.Pi
30. }
31. func (c circle) Perimeter() float64 {
32. return 2*c.R*math.Pi
33. }
34.
35. func main() {
36. var s1,s2 ShapeDesc
37. s1 = rectangle{H:2,W:3} //注意此处,rectangle实现了ShapeDesc接口
38. s2 = circle{R:2} //注意此处,circle实现了ShapeDesc接口
39. Desc(s1)
40. Desc(s2)
41. }
42.
43. func Desc(s ShapeDesc) {
44. _,ok := s.(circle)
45. if ok{
46. fmt.Println("This is circle.")
47. }
48. _,ok = s.(rectangle)
49. if ok{
50. fmt.Println("This is rectangle.")
51. }
52. fmt.Println("area:",s.Area())
53. fmt.Println("perimeter:",s.Perimeter())
54. }
55.
56. //以下是运行结果
57. This is rectangle.
58. area: 6
59. perimeter: 10
60. This is circle.
61. area: 12.566370614359172
62. perimeter: 12.566370614359172
第8行至第11行,定义了一个接口,接口里面定义了两个函数:Area和Perimeter。
第13行至第19行,定义了两个struct:一个矩形和一个圆形。
第21行至第26行,为矩形struct定义了两个方法:一个Area和一个Perimeter。这两个函数就是最开始接口ShapeDesc定义的函数,所以矩形struct实现了接口ShapeDesc。
第28行至第33行,圆形struct也实现了接口ShapeDesc。
第43行至第54行,该函数以ShapeDesc类型作为形参,函数体根据参数判断类型打印不同的语句,然后分别执行Area和Perimeter方法。此处要注意第44行和第48行判断类型的用法。
第36行至第40行,首先定义ShapeDesc接口类型的两个变量s1和s2,第37行和第38行又分别定义两个变量,即rectangle和circle类型,这两个类型可以赋给s1和s2,因为它们都实现了接口ShapeDesc。第39行和第40行分别调用了函数Desc,可以查看第57行至第62行的打印结果。
注意,第43行至第54行的函数也可以采用如下写法:
1. func Desc(s ShapeDesc) { 2. switch kind :=s.(type) { 3. case circle: 4. fmt.Println("This is circle.") 5. case rectangle: 6. fmt.Println("This is rectangle.") 7. default: 8. fmt.Println("%v is unknown type",kind) 9. } 10. 11. fmt.Println("area:",s.Area()) 12. fmt.Println("perimeter:",s.Perimeter()) 13. }
Desc函数的两个方式也是接口断言经常用的。虽然有些类型实现了某接口并且使用对应接口类型的变量来存储具体类型的变量,但是这时候变量只能调用接口所定义的方法,不能调用具体类型拥有而接口没有的方法,这时就需要通过断言转换变量类型后再去调用,也就是说接口断言派上用场了。
上面就是Go语言中的类型断言。因为接口的值是动态的,需要判断其具体类型是什么、接口类型是什么,这种操作就是类型断言。
说明
Go语言的类型断言可以用x.(T)表达,其中x是一个接口类型的具体值表达式,而T是一个类型—断言类型。T的主要作用就是检查动态值x是否满足T。
接口类型的值是如何存储的呢?接口类型包括两部分,即一个具体类型和该类型的一个值,分别称为动态类型和动态值。为什么称其为动态类型和动态值呢?这是因为Go语言作为一种静态语言,经过编译以后就没有严格意义上的类型值了,所以需要通过类型描述来描述类型的具体信息,以提供给编译器使用。还是通过一个代码示例看一下接口类型值的变化过程:
book/ch04/4.3/adv/main.go
1. package main
2.
3. import "fmt"
4.
5. type IPrint interface {
6. MyPrint()
7. }
8.
9. type IS1 struct {
10. A,B int
11. S string
12. }
13.
14. type IS2 struct {
15. S string
16. }
17.
18. func main() {
19. var is1 IPrint
20. s1 := IS1{A:1,B:1,S:"hello"}
21. is1.MyPrint() //运行会报错
22. is1 = s1
23. is1.MyPrint()
24. fmt.Println(is1.S) //编译报错,Iprint接口没有S
25. is1 = IS2{S:"hello world"}
26. is1.MyPrint()
27. }
28.
29. func (i IS1) MyPrint() {
30. fmt.Println(i.S)
31. }
32.
33. func (i IS2) MyPrint() {
34. fmt.Println(i.S)
35. }
第5行至第7行,定义了一个接口,仅含有一个函数MyPrint。
第9行至第16行,定义了两个struct。
第29行至第35行,分别为前面定义的两个struct实现了接口IPrint。
第19行至第26行是要重点介绍的部分。首先在第19行定义了IPrint接口类型变量is1,在第19行执行完成后,其接口类型值(或者简称为接口值)如下。
类型:nil
值:nil
然后定义了一个IS1结构体,变量为s1,这时候在内存当中有了s1的一块内存空间,其地址就是*s1。
第21行做了个假设,事实上这行代码是无法执行的,因为当前is1的接口类型值的值部分是nil,去调用MyPrint方法肯定会报错。
第22行把s1赋值给is1,这时候is1的接口类型和值如下。
类型:*s1
值:具体的s1的内存空间
第23行再去调用MyPrint方法的时候,就相当于执行了(*s1).MyPrint。那么能不能通过is1.S的方式来调用(*s1).S呢?答案是否定的,因为接口里面只有MyPrint方法,所以第24行编译无法通过。
第25行是把is1的接口类型值变为如下内容。
类型:*IS2{S: “hello world”}
值:上述类型的具体内存空间
这时候再通过第26行调用MyPrint方法的时候,打印的是“hello world”。
注意
接口类型的值(或简称接口值)包括动态类型和动态值,也就是说在编译阶段并不知道具体的类型和值,而是在程序执行到此时再通过动态类型和动态值去调用具体的方法。所以读者在思考接口运行方式时,始终要将接口看作动态类型和动态值两个字段,这样更有利于理解。
另外,还需要对空接口进行特殊说明,在4.1节介绍函数的时候已经使用过空接口的概念,即可以使用interface{}定义接收任何类型的接口。空接口类似于其他面向对象语言的Object。
Go语言的接口很灵活,这里不再一一介绍其特征,下面是使用Go语言要注意的内容:
▪接口中只能声明方法,不可以有具体实现。
▪接口中不可以声明变量,仅允许声明方法。
▪实现一个接口,就必须实现接口内声明的所有方法。
▪实现一个方法就是要和接口声明的方法的方法名、形参、返回值完全一致。
▪接口也是可以嵌套组合的,和结构体一样。
▪在接口中声明方法时,不可以出现重名方法。
本节介绍的接口,与上一节介绍的方法结合起来可以非常好地完成面向对象的设计,虽然没有用到继承,但是也可以达到继承的效果,而且接口在使用上更为灵活。Go语言的理念是没必要把数据和方法封装在一起,只需要通过接口将逻辑进行抽象和组合即可,这与C++、Java完全不同。接口是Go语言实现面向对象的最重要方式。基于Go语言的这种Interface方式,让面向对象一下子简单了很多,编程时不需要再去考虑是使用多重继承还是将继承和接口分开,也不需要去考虑多态的问题,这让工程师可以更多地关注逻辑而不是编译器的设计。Go语言的面向对象依赖于接口,而不是依赖于实现。