golang中interface接口的深度解析

一 接口介绍

如果说gorountine和channel是支撑起Go语言的并发模型的基石,让Go语言在如今集群化与多核化的时代成为一道亮丽的风景,那么接口是Go语言整个类型系列的基石,让Go语言在基础编程哲学的探索上达到前所未有的高度。Go语言在编程哲学上是变革派,而不是改良派。这不是因为Go语言有gorountine和channel,而更重要的是因为Go语言的类型系统,更是因为Go语言的接口。Go语言的编程哲学因为有接口而趋于完美。C++,Java 使用"侵入式"接口,主要表现在实现类需要明确声明自己实现了某个接口。这种强制性的接口继承方式是面向对象编程思想发展过程中一个遭受相当多质疑的特性。Go语言采用的是“非侵入式接口",Go语言的接口有其独到之处:只要类型T的公开方法完全满足接口I的要求,就可以把类型T的对象用在需要接口I的地方,所谓类型T的公开方法完全满足接口I的要求,也即是类型T实现了接口I所规定的一组成员。这种做法的学名叫做Structural Typing,有人也把它看作是一种静态的Duck Typing。

要这个值实现了接口的方法。

type Reader interface {
 Read(p []byte) (n int, err os.Error)
} 

// Writer 是包裹了基础 Write 方法的接口。
type Writer interface {
 Write(p []byte) (n int, err os.Error)
} 

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer) 

有一个事情是一定要明确的,不论 r 保存了什么值,r 的类型总是 io.Reader ,Go 是静态类型,而 r 的静态类型是 io.Reader。接口类型的一个极端重要的例子是空接口interface{},它表示空的方法集合,由于任何值都有零个或者多个方法,所以任何值都可以满足它。也有人说 Go 的接口是动态类型的,不过这是一种误解。 它们是静态类型的:接口类型的变量总是有着相同的静态类型,这个值总是满足空接口,只是存储在接口变量中的值运行时可能被改变。对于所有这些都必须严谨的对待,因为反射和接口密切相关。

二  接口类型内存布局

在类型中有一个重要的类别就是接口类型,表达了固定的一个方法集合。一个接口变量可以存储任意实际值(非接口),只要这个值实现了接口的方法。interface在内存上实际由两个成员组成,如下图,tab指向虚表,data则指向实际引用的数据。虚表描绘了实际的类型信息及该接口所需要的方法集。

type Stringer interface {
 String() string
} 

type Binary uint64 

func (i Binary) String() string {
 return strconv.FormatUint(i.Get(), 2)
} 

func (i Binary) Get() uint64 {
 return uint64(i)
} 

func main() {
 var b Binary = 32
 s := Stringer(b)
 fmt.Print(s.String())
} 

观察itable的结构,首先是描述type信息的一些元数据,然后是满足Stringger接口的函数指针列表(注意,这里不是实际类型Binary的函数指针集哦)。因此我们如果通过接口进行函数调用,实际的操作其实就是s.tab->fun[0](s.data) 。是不是和C++的虚表很像?但是他们有本质的区别。先看C++,它为每个类创建了一个方法集即虚表,当子类重写父类的虚函数时,就将表中的相应函数指针改为子类自己实现的函数,如果没有则指向父类的实现,当面临多继承时,C++对象结构里就会存在多个虚表指针,每个虚表指针指向该方法集的不同部分。我们再来看golang的实现方式,同C++一样,golang也为每种类型创建了一个方法集,不同的是接口的虚表是在运行时专门生成的,而c++的虚表是在编译时生成的(但是c++虚函数表表现出的多态是在运行时决定的).例如,当例子中当首次遇见s := Stringer(b)这样的语句时,golang会生成Stringer接口对应于Binary类型的虚表,并将其缓存。那么为什么go不采用c++的方式来实现呢?这根c++和golang的对象内存布局是有关系的。

首先c++的动态多态是以继承为基础的,在对象构造初始化的时首先会初始化父类,其次是子类,也就是说一个对象的内存布局是虚表,父类部分,子类部分(编译器不同可能会有差异),当一个父类指针指向子类时,会发生内存的截断,截断子类部分(内存地址偏移),但是此时子类的虚表中的函数指针实际上还是指向了自己的实现,所以此时的指针才会调用到子类的虚函数,如果不是虚函数,因为内存已经截断没有子类的非虚函数信息了,所以只能调用父类的了,这种继承关系让c++的虚表的初始化非常清晰,在一个对象初始化时先调用父类的构造此时虚表跟父类是一样的,接下来初始化子类,此时编译器就会去识别子类有没有覆盖父类的虚函数,如果有则虚表中相应的函数指针改成自己的虚函数实现指针。

那么go有什么不同呢,首先我们很清楚go是没有严格意义上的继承的,go的接口不存在继承关系,只要实现了接口定义的方法都可以成为接口类型,这给go的虚表初始化带来很大的麻烦,到底有多少类型实现了这个接口,一个类型到底实现了多少接口这让编译器很confused。举个例子,某个类型有m个方法,某接口有n个方法,则很容易知道这种判定的时间复杂度为O(mXn),不过可以使用预先排序的方式进行优化,实际的时间复杂度为O(m+n)这样看来其实还行那为什么要在运行时生成虚表呢,这不是会拖慢程序的运行速度吗,注意我们这里是某个类型,某个接口,是1对1的关系,如果有n个类型,n个接口呢,编译器难道要把之间所有的关系都理清吗?退一步说就算编译器任劳任怨把这事干了,可是你在写过程中你本来就不想实现那个接口,而你无意中给这个类型实现的方法中包含了某些接口的方法,你根本不需要这个接口(况且go的接口机制会导致很多这种无意义的接口实现),你欺负编译器就行了,这也太欺负人了吧。如果我们放到运行时呢,我们只要在需要接口的去分析一下类型是否实现了接口的所有方法就行了很简单的一件事。

三 空接口

接口类型的一个极端重要的例子是空接口:interface{} ,它表示空的方法集合,由于任何值都有零个或者多个方法,所以任何值都可以满足它。 注意,[]T不能直接赋值给[]interface{}

//t := []int{1, 2, 3, 4} wrong
//var s []interface{} = t
t := []int{1, 2, 3, 4} //right
s := make([]interface{}, len(t))
for i, v := range t {
 s[i] = v
} 
str, ok := value.(string)
if ok {
 fmt.Printf("string value is: %q\n", str)
} else {
 fmt.Printf("value is not a string\n")
} 

在Go语言中,我们可以使用type switch语句查询接口变量的真实数据类型,语法如下:

type Stringer interface {
  String() string
} 

var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
  return str //type of str is string
case Stringer: //type of str is Stringer
  return str.String()
} 

也可以使用“comma, ok”的习惯用法来安全地测试值是否为一个字符串:

str, ok := value.(string)
if ok {
  fmt.Printf("string value is: %q\n", str)
} else {
  fmt.Printf("value is not a string\n")
} 

四 接口赋值

package main 

import (
"fmt"
) 

type LesssAdder interface {
  Less(b Integer) bool
  Add(b Integer)
} 

type Integer int 

func (a Integer) Less(b Integer) bool {
  return a < b
} 

func (a *Integer) Add(b Integer) {
  *a += b
} 

func main() { 

  var a Integer = 1
  var b LesssAdder = &a
  fmt.Println(b) 

  //var c LesssAdder = a
  //Error:Integer does not implement LesssAdder
  //(Add method has pointer receiver)
} 

go语言可以根据下面的函数:

func (a Integer) Less(b Integer) bool  

自动生成一个新的Less()方法

func (a *Integer) Less(b Integer) bool  

这样,类型*Integer就既存在Less()方法,也存在Add()方法,满足LessAdder接口。 而根据

func (a *Integer) Add(b Integer) 

这个函数无法生成以下成员方法:

func(a Integer) Add(b Integer) {
  (&a).Add(b)
} 

因为(&a).Add()改变的只是函数参数a,对外部实际要操作的对象并无影响(值传递),这不符合用户的预期。所以Go语言不会自动为其生成该函数。因此类型Integer只存在Less()方法,缺少Add()方法,不满足LessAddr接口。(可以这样去理解:指针类型的对象函数是可读可写的,非指针类型的对象函数是只读的)将一个接口赋值给另外一个接口 在Go语言中,只要两个接口拥有相同的方法列表(次序不同不要紧),那么它们就等同的,可以相互赋值。 如果A接口的方法列表时接口B的方法列表的子集,那么接口B可以赋值给接口A,但是反过来是不行的,无法通过编译。

五 接口查询

接口查询是否成功,要在运行期才能够确定。他不像接口的赋值,编译器只需要通过静态类型检查即可判断赋值是否可行。

var file1 Writer = ...
if file5,ok := file1.(two.IStream);ok {
...
}

这个if语句检查file1接口指向的对象实例是否实现了two.IStream接口,如果实现了,则执行特定的代码。

在Go语言中,你可以询问它指向的对象是否是某个类型,比如,

var file1 Writer = ...
if file6,ok := file1.(*File);ok {
...
}

这个if语句判断file1接口指向的对象实例是否是*File类型,如果是则执行特定的代码。

slice := make([]int, 0)
slice = append(slice, 1, 2, 3)

var I interface{} = slice

if res, ok := I.([]int);ok {
  fmt.Println(res) //[1 2 3]
}

这个if语句判断接口I所指向的对象是否是[]int类型,如果是的话输出切片中的元素。

func Sort(array interface{}, traveser Traveser) error {

  if array == nil {
    return errors.New("nil pointer")
  }
  var length int //数组的长度
  switch array.(type) {
  case []int:
    length = len(array.([]int))
  case []string:
    length = len(array.([]string))
  case []float32:
    length = len(array.([]float32))

  default:
    return errors.New("error type")
  }

  if length == 0 {
    return errors.New("len is zero.")
  }

  traveser(array)

  return nil
}

通过使用.(type)方法可以利用switch来判断接口存储的类型。

小结: 查询接口所指向的对象是否为某个类型的这种用法可以认为是接口查询的一个特例。接口是对一组类型的公共特性的抽象,所以查询接口与查询具体类型区别好比是下面这两句问话的区别:

你是医生么?

是。

你是莫莫莫

第一句问话查询的是一个群体,是查询接口;而第二个问句已经到了具体的个体,是查询具体类型。

除此之外利用反射也可以进行类型查询,会在反射中做详细介绍。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

时间: 2017-11-12

Go语言interface详解

interface Go语言里面设计最精妙的应该算interface,它让面向对象,内容组织实现非常的方便,当你看完这一章,你就会被interface的巧妙设计所折服. 什么是interface 简单的说,interface是一组method的组合,我们通过interface来定义对象的一组行为. 我们前面一章最后一个例子中Student和Employee都能SayHi,虽然他们的内部实现不一样,但是那不重要,重要的是他们都能say hi 让我们来继续做更多的扩展,Student和Employe

Go语言interface 与 nil 的比较

interface简介 Go语言以简单易上手而著称,它的语法非常简单,熟悉C++,Java的开发者只需要很短的时间就可以掌握Go语言的基本用法. interface是Go语言里所提供的非常重要的特性.一个interface里可以定义一个或者多个函数,例如系统自带的io.ReadWriter的定义如下所示: type ReadWriter interface { Read(b []byte) (n int, err error) Write(b []byte) (n int, err error)

go语言中的interface使用实例

go语言中的interface是一组未实现的方法的集合,如果某个对象实现了接口中的所有方法,那么此对象就实现了此接口.与其它面向对象语言不同的是,go中无需显示声明调用了哪个接口. 复制代码 代码如下: package main   import (  "fmt" )   type I interface {  Get() int  Put(int) }   type S struct{ i int }   func (p *S) Get() int  { return p.i } f

浅谈Go语言中的结构体struct & 接口Interface & 反射

结构体struct struct 用来自定义复杂数据结构,可以包含多个字段(属性),可以嵌套: go中的struct类型理解为类,可以定义方法,和函数定义有些许区别: struct类型是值类型. struct定义 type User struct { Name string Age int32 mess string } var user User var user1 *User = &User{} var user2 *User = new(User) struct使用 下面示例中user1和

浅谈c语言中一种典型的排列组合算法

c语言中的全排列算法和组合数算法在实际问题中应用非常之广,但算法有许许多多,而我个人认为方法不必记太多,最好只记熟一种即可,一招鲜亦可吃遍天 全排列: #include<stdio.h> void swap(int *p1,int *p2) { int t=*p1; *p1=*p2; *p2=t; } void permutation(int a[],int index,int size) { if(index==size) { for(int i=0;i<size;i++) print

浅谈c语言中类型隐性转换的坑

谨记:在C语言中,当两种不同类型之间运算时,低字节长度类型会向高自己长度类型转换,有符号会向无符号类型转换. 举例子如下: #include <stdio.h> void func(void) { int i = 1; unsigned char c1 = 1; signed char c2 = -1; if (c2 > i){ printf("\r\n -1 > 1"); } else{ printf("\r\n -1 <= 1");

浅谈c语言中转义字符的用法及注意事项

c语言中的转义字符: \a 响铃符 \b 退格 \f 换页符 \n 换行符 \r 回车符(回到该行的首位置) \v 纵向制表符 \\ 反斜杠 \? 问号(?经vs10测试可以直接打印) \"(\') 双引号(单引号) \ooo 八进制数(ooo表示一个用8进制数表示出来的对应ANSII代码对应出字符,用此方法可以表示出所有ASCII字符.不过测试发现打不出%号,存疑!) \xhh 十六进制数(功能同八进制数,用hh表示一个十六进制数,如\x20表示空格) 注:使用转义字符的退格符,换行符,回车符

浅谈C语言中的注释风格小结

C语言中常用的注释风格有两种,一种是通过如下模式进行一段代码的注释: /* comment*/ 另一种是单行注释符号: // comment 学生时代的注释我一般是选用后者,那时候编码量十分有限,即使是简单的小段落注释使用的IDE也支持批量添加单行注释符.而在编码之中,简单的单行注释进行注释的时候键盘的操作更为简单一点. 不过,工作之后接触了相应的编码规范之后,C语言的注释我基本上放弃了单行注释的方法,最多仅仅在调试的时候做简单的使用. 其实,单行注释是从C++中借鉴来的,算是C++风格的注释方

C语言中隐藏结构体的细节

我们都知道,在C语言中,结构体中的字段都是可以访问的.或者说,在C++ 中,类和结构体的主要区别就是类中成员变量默认为private,而结构体中默认为public.结构体的这一个特性,导致结构体中封装的数据,实际上并没有封装,外界都可以访问结构体重的字段. C++中我们尚可用类来替代结构体,但是,C语言中是没有类的,只能用结构体,但很多时候,我们需要隐藏结构体的字段,不让外界直接访问,而是通过我们写的函数进行间接访问,这样就提高了程序的封装性. 实现方法,简单来说,就是,结构体定义时,要定义在.

浅谈C语言中结构体的初始化

<代码大全>建议在变量定义的时候进行初始化,但是很多人,特别是新人对结构体或者结构体数组定义是一般不会初始化,或者不知道怎么初始化.1.初始化 复制代码 代码如下: typedef struct _TEST_T {        int i;        char c[10];}TEST_T;TEST_T gst  = {1, "12345"};//可以初始化,设置i为1,s为一个字符串.TEST_T gst  = {1};//初始化个数少于实际个数时,只初始化前面的成员

C语言中的结构体的入门学习教程

C语言中数组允许定义类型的变量,可容纳相同类型的多个数据项,但结构体在C语言编程中,它允许定义不同种类的数据项可供其他用户定义的数据类型. 结构是用来代表一个记录,假设要跟踪图书馆的书籍.可能要跟踪有关每本书以下属性: Title - 标题 Author - 作者 Subject - 科目 Book ID - 编号 定义结构体 定义一个结构体,必须使用结构体的struct语句.该struct语句定义了一个新的数据类型,程序不止一个成员.struct语句的格式是这样的: struct [struc

浅谈C语言中的强符号、弱符号、强引用和弱引用

首先我表示很悲剧,在看<程序员的自我修养--链接.装载与库>之前我竟不知道C有强符号.弱符号.强引用和弱引用.在看到3.5.5节弱符号和强符号时,我感觉有些困惑,所以写下此篇,希望能和同样感觉的朋友交流也希望高人指点. 首先我们看一下书中关于它们的定义. 引入场景:(1)文件A中定义并初始化变量i(int i = 1), 文件B中定义并初始化变量i(int i = 2).编译链接A.B时会报错b.o:(.data+0x0): multiple definition of `i':a.o:(.d

浅谈Go语言中字符串和数组

go语言里边的字符串处理和PHP还有java 的处理是不一样的,首先申明字符串和修改字符串 复制代码 代码如下: package main import "fmt" var name string           //申明一个字符串 var emptyname string = "" //申明一个空字符串 func main() {  //申明多个字符串并且赋值  a, b, v := "hello", "word", &