|
|
51CTO旗下网站
|
|
移动端

9.1.1 接口是什么

《Go语言编程入门与实战技巧》第9章接口与反射,本章需要注意设计类型时确认类型的本质是原始的还是非原始的。接口是声明了一组行为并支持多态的类型,嵌入类型提供了扩展类型的能力,而无须使用继承。本节为大家介绍接口是什么。

作者:黄靖钧来源:电子工业出版社|2018-09-23 09:19

9.1.1  接口是什么

Go语言是"非传统"的面向对象编程语言,它没有类和继承的概念,但是Go语言里有非常灵活的接口概念,通过它可以实现很多面向对象的特性。接口提供了一种方式来说明对象的行为:如果谁能搞定这件事,它就可以在这里调用。

接口定义了一组方法(方法集),但是这些方法不包含(实现)代码--它们没有被实现(它们是抽象的),接口里也不能包含变量。

通过如下格式定义接口:

  1. type Namer interface {  
  2.     Method1(param_list) return_type  
  3.     Method2(param_list) return_type  
  4.     ...  
  5. }  

上面的Namer是一个接口类型。按照约定,只包含一个方法的接口的名字由方法名加[e]r后缀组成,例如Printer、Reader、Writer、Logger、Converter等。还有一些不常用的方式(当后缀er不合适时),比如Recoverable,此时接口名以able结尾,或者以I开头。

Go语言中的接口都很简短,通常它们会包含0~3个方法。不像大多数面向对象编程语言,在Go语言中接口可以有值,一个接口类型的变量或一个接口值,例如var in Namer,其中in是一个多字(multiword)数据结构,它的值为nil,它本质上是一个指针,虽然不完全是一回事。指向接口值的指针是非法的,它们不仅一点用也没有,还会导致代码错误。此处的方法指针表是通过运行时反射能力构建的。

类型(比如结构体)实现接口方法集中的方法,每一个方法的实现说明了此方法是如何作用于该类型的:即实现接口,同时方法集也构成了该类型的接口。实现了Namer接口类型的变量可以赋值给in(接收者值),此时方法表中的指针会指向被实现的接口方法。当然,如果另一个类型(也实现了该接口)的变量被赋值给ai,对应指针和方法实现也会随之改变。

类型不需要显式声明它实现了某个接口:接口被隐式实现。多个类型可以实现同一个接口,实现某个接口的类型(除了实现接口方法外)可以有其他的方法。一个类型可以实现多个接口,接口类型可以包含一个实例的引用,该实例的类型实现了此接口(接口是动态类型)。

即使接口在类型实现之后才定义,二者处于不同的包中,被单独编译,只要类型实现了接口中的方法,它就实现了此接口,所有这些特性使得接口具有很大的灵活性。

  1. // 示例代码9-1  
  2. package main  
  3.  
  4. import "fmt"  
  5.  
  6. type Shaper interface {  
  7.     Area() float32  
  8. }  
  9.  
  10. type Square struct {  
  11.     side float32  
  12. }  
  13.  
  14. func (sq *Square) Area() float32 {  
  15.     return sq.side * sq.side  
  16. }  
  17.  
  18. func main() {  
  19.     sq1 :new(Square)  
  20.     sq1.side = 5 
  21.  
  22.     // 定义 areaIntf  
  23.     // areaIntf = sq1 
  24.     // 更简洁,不需要分开定义:  
  25.     // areaIntf :Shaper(sq1)  
  26.     // 甚至这样:  
  27.     areaIntf :sq1 
  28.     fmt.Printf("面积为:%f\n", areaIntf.Area())  
  29. }  
  30. /*  
  31. 返回:  
  32. 面积为:25.000000  
  33. */  

上面的程序定义了一个结构体Square和一个接口Shaper,接口有一个方法Area()。在main()方法中创建了一个Square的实例。在主程序外边定义了一个接收者类型是Square方法的Area(),用来计算正方形的面积,结构体Square实现了接口Shaper。

所以可以将一个Square类型的变量赋值给一个接口类型的变量:areaIntf = sq1 。

现在接口变量包含一个指向Square变量的引用,通过它可以调用Square上的方法Area()。当然也可以直接在Square的实例上调用此方法,但是在接口实例上调用此方法更具通用性。接口变量里包含了接收者实例的值和指向对应方法表的指针。

这是Go语言版本的"多态",多态是面向对象编程中一个广为人知的概念:根据当前的类型选择正确的方法,或者说同一种类型在不同的实例上似乎表现出不同的行为。

如果Square没有实现Area()方法,编译器将会给出清晰的错误信息:

  1. cannot use sq1 (type *Square) as type Shaper in assignment:  
  2. *Square does not implement Shaper (missing Area method)  

如果Shaper有另外一个方法Perimeter(),但是Square没有实现它,即使没有人在Square实例上调用这个方法,编译器也会给出上面的错误。

扩展上面的例子,类型Rectangle也实现了Shaper接口。接着创建一个Shaper类型的数组,迭代它的每一个元素并在上面调用Area()方法,以此来展示多态行为:

  1. // 示例代码9-2  
  2. package main  
  3.  
  4. import "fmt"  
  5.  
  6. type Shaper interface {  
  7.     Area() float32  
  8. }  
  9.  
  10. type Square struct {  
  11.     side float32  
  12. }  
  13.  
  14. func (sq *Square) Area() float32 {  
  15.     return sq.side * sq.side  
  16. }  
  17.  
  18. type Rectangle struct {  
  19.     length, width float32  
  20. }  
  21.  
  22. func (r Rectangle) Area() float32 {  
  23.     return r.length * r.width  
  24. }  
  25.  
  26. func main() {  
  27.  
  28.     r :Rectangle{5, 3} // 需要的是一个值  
  29.     q := &Square{5}      // 传入的是指针  
  30.     // shapes := []Shaper{Shaper(r), Shaper(q)}  
  31.     // 更简洁的写法:  
  32.     shapes := []Shaper{r, q}  
  33.     // 遍历 shapes  
  34.     for n, _ :range shapes {  
  35.         fmt.Println("形状参数:", shapes[n])  
  36.         fmt.Println("形状面积是:", shapes[n].Area())  
  37.     }  
  38. }  
  39. /*  
  40. 返回:  
  41. 形状参数:{5 3}  
  42. 形状面积是:15  
  43. 形状参数:&{5}  
  44. 形状面积是:25  
  45. */  

在调用shapes[n].Area()时,只知道shapes[n]是一个Shaper对象,最后它摇身一变成为了一个Square或Rectangle对象,并且表现出了相对应的行为。

也许从现在开始你将看到通过接口如何产生更干净、更简单及更具扩展性的代码。在后面章节将看到在开发中为类型添加新的接口是多么容易。

下面是一个更具体的例子:有两个类型stockPosition和car,它们都有一个getValue()方法,可以定义一个具有此方法的接口valuable。接着定义一个使用valuable类型作为参数的函数showValue(),所有实现了valuable接口的类型都可以用这个函数。

  1. // 示例代码9-3  
  2. package main  
  3.  
  4. import "fmt"  
  5.  
  6. type stockPosition struct {  
  7.     ticker     string  
  8.     sharePrice float32  
  9.     count      float32  
  10. }  
  11.  
  12. /* 获取 stock 的值(价格) */  
  13. func (s stockPosition) getValue() float32 {  
  14.     return s.sharePrice * s.count  
  15. }  
  16.  
  17. type car struct {  
  18.     make  string  
  19.     model string  
  20.     price float32  
  21. }  
  22.  
  23. /* 获取 car 的值(价格) */  
  24. func (c car) getValue() float32 {  
  25.     return c.price  
  26. }  
  27.  
  28. /* 定义具有价值的不同事物的“合同” */  
  29. type valuable interface {  
  30.     getValue() float32  
  31. }  
  32.  
  33. func showValue(asset valuable) {  
  34.     fmt.Printf("资产的价值是:%f\n", asset.getValue())  
  35. }  
  36.  
  37. func main() {  
  38.     var o valuable = stockPosition{"GOOG", 577.20, 4}  
  39.     showValue(o)  
  40.     o = car{"BMW", "M3", 66500}  
  41.     showValue(o)  
  42. }  
  43. /*  
  44. 返回:  
  45. 资产的价值是:2308.800049  
  46. 资产的价值是:66500.000000  
  47. */  

举一个标准库的例子,io包里有一个接口类型Reader:

  1. type Reader interface {  
  2.     Read(p []byte) (n int, err error)  
  3. }  

定义变量var r io.Reader,那么就可以写如下的代码:

  1. var r io.Reader  
  2.     r = os.Stdin  
  3.     r = bufio.NewReader(r)  
  4.     r = new(bytes.Buffer)  
  5.     f,_ :os.Open("test.txt")  
  6.     r = bufio.NewReader(f)  

上面r右边的类型都实现了Read()方法,并且有相同的方法签名,r的静态类型是io.Reader。

注意,有的时候,也会以一种稍微不同的方式来使用接口这个词,从某个类型的角度来看,它的接口指的是:它的所有导出方法,只不过没有显式地为这些导出方法额外指定一个接口而已。


喜欢的朋友可以添加我们的微信账号:

51CTO读书频道二维码


51CTO读书会第9群:808517103

【责任编辑:book TEL:(010)68476606】

回书目   上一节   下一节
点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

Struts 2权威指南:基于WebWork核心的MVC开发

本书所介绍的Struts 2已经完全超出了Struts 1框架原有的高度,Struts 2建立在Struts 1和WebWork两个框架整合的基础之上,因此提供了更多优...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊