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

9.1.3 接口实现

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

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

9.1.3  接口实现

一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。例如,*os.File类型实现了io.Reader、Writer、Closer和ReadWriter接口。

*bytes.Buffer实现了Reader、Writer和ReadWriter这些接口,但是它没有实现Closer接口,所以它不具有Close方法。Go语言的程序员经常会简要地把一个具体的类型描述成一个特定的接口类型。

举个例子,*bytes.Buffer是io.Writer;*os.Files是io.ReadWriter。接口指定的规则非常简单:表达一个类型属于某个接口只要这个类型实现这个接口就可以,这个规则甚至适用于等式右边本身也是一个接口类型的情况,因为ReadWriter和ReadWriteCloser包含所有Writer的方法,所以任何实现了ReadWriter和ReadWriteCloser的类型必定也实现了Writer接口。

在进一步学习前,必须先解释表示一个类型持有一个方法当中的细节。对于每一个命名过的具体类型T,它的一些方法的接收者是类型T本身,然而另一些则是一个T的指针。还记得在T类型的参数上调用一个T的方法是合法的,只要这个参数是一个变量。

编译器隐式地获取了它的地址,但这仅仅是一个语法糖:T类型的值不拥有所有*T指针的方法,那这样它就可能只实现更少的接口。

举个例子可能会更清晰一点,IntSet类型的String方法的接收者是一个指针类型,所以我们不能在一个不能寻址的IntSet值上调用这个方法:

  1. type IntSet struct {  
  2.     /* .... */  
  3. }  
  4. func (*IntSet) String() string  
  5. var _ = IntSet{}.String()  
  6. // 编译错误返回:String requires *IntSet receiver  

但是可以在一个IntSet值上调用这个方法:

  1. var s IntSet  
  2. var _ = s.String()  
  3. // 编译通过:s 是一个变量,&s 有一个String方法  

然而,由于只有IntSet类型有String方法,所有也只有IntSet类型实现了fmt.Stringer接口:

  1. var _ fmt.Stringer = &s // 编译通过  
  2. var _ fmt.Stringer = s  // 编译错误返回:IntSet lacks String method  

下面是接口在runtime中的实现,注意其中包含了接口本身和实际数据类型的类型信息:

  1. // src/runtime/runtime2.go  
  2. type iface struct {  
  3.     // 包含接口的静态类型信息、数据的动态类型信息和函数表  
  4.     tab *itab  
  5.     /* 指向具体数据的内存地址,比如slice、map等,或者在接口转换时直接存放小数据(一个  指针的长度) */  
  6.     data unsafe.Pointer  
  7. }  
  8. type itab struct {  
  9.     // 接口的类型信息  
  10.     inter *interfacetype  
  11.     // 具体数据的类型信息  
  12.     _type  *_type  
  13.     link   *itab  
  14.     hash   uint32  
  15.     bad    bool  
  16.     inhash bool  
  17.     unused [2]byte  
  18.     /* 函数地址表,这里放置和接口方法对应的具体数据类型的方法地址  
  19.        实现接口调用方法的动态分派,一般在每次给接口赋值发生转换时  
  20.        会更新此表,或者直接获取缓存的itab */  
  21.     fun [1]uintptr // 变量大小  
  22. }  

另外,需要注意与接口相关的两点优化,会影响到反射等的实现。

(1)空接口(interface{})的itab优化:当将某个类型赋值给空接口时,由于空接口没有方法,所以上面空接口iface的tab字段会直接指向数据的具体类型。在Go语言的reflect包中,reflect.TypeOf和reflect.ValueOf的参数都是空接口,因此所有参数都会先转换为空接口类型,这样反射就实现了对所有参数类型获取实际数据类型的统一,这在后面反射的基本实现中会分析到。

(2)发生"接口转换"时data字段相关的优化:当被转换为接口的数据的类型长度不超过一个指针的长度时(比如pointer、map、func、chan、[1]int等类型),接口转换时会将数据直接复制到接口的data字段(DirectIface)中,而不再额外分配内存并复制。另外,从Go 1.8+的源码来看除DirectIface的优化以外,还对长度较小(不超过64B,未初始化数据内存的array、空字符串等)的零值做了优化,也不会重新分配内存,而是直接指向一个包级全局数组变量zeroVal的首地址。注意这里的优化发生在接口转换时生成的临时接口上,而不是被赋值的接口左值上。

再者,在Go语言中只有值传递(包括接口类型),与具体的类型实现无关,但是某些类型具有引用的属性。典型的9种非基础类型中:

- array传递会复制整块数据内存,传递长度为len(arr) * Sizeof(elem)。

- string、slice、interface传递的是其runtime的实现,所以长度是固定的,分别为16B、24B、16B(AMD64)。

- map、func、chan、pointer传递的是指针,所以长度固定为8B(AMD64)。

- struct传递的是所有字段的内存副本,所以长度是所有字段的长度之和。


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

51CTO读书频道二维码


51CTO读书会第9群:808517103

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

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

读 书 +更多

Visual Studio Team Systems软件工程实践

本书论述了软件开发价值增加的思维方式。这一思维方式构成了VSTS的基础,包括VSTS的指导思想,为什么这些指导思想会以某些方式表现,以及它...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊