[译] Go数据结构-接口

原文 Go Data Structures: Interfaceshtml

做者 Russ Coxgolang

声明:本文目的仅仅做为我的mark,因此在翻译的过程当中参杂了本身的思想甚至改变了部份内容。但因为译者水平有限,所写文字或者代码可能会误导读者,如发现文章有问题,请尽快告知,不胜感激。c#


一些知识点

  1. Method Set方法集合,Go中每一个类型都有其与之关联的方法集合,interface类型的方法集合是其接口,除了interface类型的其余类型T的方法集合是全部receiverT的全部方法,而类型*T的方法集合则是receiver*T或者T的全部方法
  2. 方法调用,若是x是类型T的实例,且表达式&x能够生成一个指向类型*T的指针,那么:假如*T的方法集合包含了someMethod方法而T没有,x.someMethod()是有效的,其本质是(&x).someMethod()

正文

Go中的接口是容许咱们使用鸭子类型,但他和某些动态语言(好比Python)不一样的是:Go编译时会捕获那些显而易见的错误,好比当接口中定义了Read()方法时,若是咱们传递int类型,或者是即便咱们传递了一个有Read()方法的类型但参数的数量或者类型和接口中定义的不一致,都会致使报错。来看一个简单的接口例子:缓存

type ReadCloser interface {
    Read(b []byte) (n int, err os.Error)
    Close()
}

而后咱们就能够定义一个接收ReadCloser类型的函数:数据结构

// 这个函数先调用Read()方法获取请求的数据而后调用Close()方法
func ReadAndClose(r ReadCloser, buf []byte) (n int, err os.Error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    r.Close()
    return
}

任何一个实现了ReadCloser中所定义的方法(不只仅是方法名相同,方法的参数数量以及对应的类型也要相同,在原文中做者称之为signatures)的类型均可以传递到ReadAndClose函数中并执行,若是咱们在某个地方传递了一个int类型过去,Go在编译的时候就会报错,可是像Python则会是在运行时报错。函数

同时,接口不只限于静态检查。咱们能够动态检查特定接口值是否具备其余方法。好比:优化

type Stringer interface {
    String() string
}

func ToString(any interface{}) string {
    if v, ok := any.(Stringer); ok {
        return v.String()
    }
    switch v := any.(type) {
    case int:
        return strconv.Itoa(v)
    case float:
        return strconv.Ftoa(v, 'g', -1)
    }
    return "???"
}

any参数被定义为空接口类型,也就是说在其中并无限定必须含有哪些方法,更进一步的说:任何类型均可以做为参数传递进来。if语句中的comma ok赋值询问是否能够将any转换为具备String方法的Stringer类型的接口值。若是是的话,接下来的语句会执行String方法并返回一个字符串。不然,switch语句则会在结束前判断其是否为几个基本类型而后执行响应的逻辑。ui

实现一个简单的例子,有一个新的64位整数类型,他有一个以二进制形式打印值的String方法,还有一个Get方法:翻译

type Binary uint64

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

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

Binary类型的值能够传递给ToString,即便程序从未说明Binary实现了Stringer接口,它也会使用String方法对其进行格式化。由于运行时能够知道Binary有一个String方法,因此它实现了Stringer,即便Binary的做者从未据说过Stringer指针

这些示例代表,即便在编译时检查了全部隐式转换,显式地接口到接口的转换也能够在运行时查询方法集。Effective Go中有更多的如何使用接口的例子。

接口值

含有"方法"概念的语言大部分都属于两个阵营中的一个(好比:C++ Java),要么是静态的为全部方法调用预置一个表,要么是调用时再查找(好比:Python)而后将其缓存起来。Go语言则是两边都沾一点:虽然他有方法集合表,但这个表是运行时构建。

先来作一个热身,Binary是一个由两个32位"字"组成的64位长的类型

Binary

接口类型的值表现为两个字(假设咱们处于32位系统中,那么一个字就是32位,本文中如没有特别声明,默认为32位系统),其中第一个字做为指针指向真正的值的元数据(包含类型,方法列表),第二个字做为指针指向真正的值。以下图所示:s := Stringer(b)赋值语句会隐式的对这两个字填充值。

接口的数据结构

接口的第一个字指向了我比较喜欢用的叫作interface table或者itable的东西。itable开头是一个存储了类型相关的元数据信息,接下来就是一个由函数指针组成的列表。注意:itable和接口类型相对应,而不是和动态类型。就咱们的例子而言:Stringer中的itable只是为了Stringer而创建,只关联了Stringer中定义的String方法,而像Binary中定义的Get方法则不在其范围内。

接口的第二个字我称之为data,其存储或者指向了实际的数据,这上面的例子中也就是指向了b。赋值语句var s Stringer = b实际上对b作了拷贝,而不是对b进行引用。存储在接口中的值可能有任意大小,但接口只提供了一个字来专门存储真实数据,因此赋值语句在堆上分配了一块内存,并将该字设置为对这块内存的引用。

ps:++itable所指向的元数据是能够被同一个类型的不一样实例所共享的,而data则无法共享。++

若是咱们想要知道接口是否内含了一个特定的类型,就像上面代码中的type swith 那样,Go编译器会产生相似于C语言中的s.tab->type表达式等效的代码来得到类型指针而后检查其是不是咱们所指望的类型。若是类型匹配,那么值会经过s.data解引用copy过去。

若是咱们想要调用s.String(),Go编译器会产生和C语言中s.tab->fun[0](s.data)表达式等效的代码。他会从itable中找到并调用对应的函数指针,而后将data中存储的数据做为第一个参数传递过去(仅仅是在本例中)。若是运行 8g -S x.go(在文章末尾有详解) 就能够看到这个过程。须要注意的是:Go编译器传递到itable中的是data中的值(32位)而不是该值所对应的Binary(64位)。一般负责执行接口调用的组件并不知道这个字表示啥,也不知道这个指针指向了多大的数据,相反的,接口代码安排itable中的函数接收接口的datada这个32位长的指向原始数据的指针的形式来做为参数传递。所以,本例中的函数指针是(*Binary).String而不是Binary.String

在本例中,咱们仅仅考虑了只有一个方法的接口的状况,而包含多个方法的接口则是在itable的尾部拥有更长的函数指针列表。

计算itable

如今咱们已经知道itable长啥样了,但咱们还不清楚他们是怎么生成的。Go的动态类型转换意味着:对于编译器或者连接器来讲,由于有太多的接口类型以及具体类型(能够说是除接口类型之外的全部类型),预先计算出全部可能的itable是不合理的,并且若是这样作的话极可能绝大多数咱们用不到。相反的,Go编译器为每个具体类型(Binary, int, func(map[int]string) 等等)生成一个用来描述类型的结构-类型描述结构。在元数据中,类型描述结构包含由该类型所实现的方法列表。类似的,编译器也会为每个接口类型(好比说: Stringer)生成一个不一样类型的类型描述结构,这个结构里面也包含了一个方法列表。接口运行时经过查找具体类型的方法表,再根据接口类型的方法表中所列出的每一个方法来计算itable。运行时会在计算出itable后将其缓存起来。因此这样只需计算一次。

在咱们的简单例子中,Stringer中的方法表中只有一个方法,而Binary的方法表中则有两个方法,一般接口可能会有 ni 个方法,而具体的类型可能会有 nt 个方法,显然为了找到具体类型方法与接口方法的映射将会须要O(ni * nt)的时间,但咱们能够作一些优化。经过对两个方法表进行排序并进行同时处理,咱们能够用O(ni + nt)的时间来完成这个映射的构建。

内存优化

咱们大致上有两种方式来进行内存优化。

首先,若是是空接口interface {},由于空接口没有定义任何方法,因此itable中的方法列表就是一个空的,也就是说其中就仅仅剩下了一个指向原始类型的指针。这种状况下,咱们就能够直接丢弃掉itable而后在第一个字中放一个指向原始类型的指针就能够了。

空接口

编译器根据一个接口是否含有方法,选用不一样的接口结构。

而后,若是原始的值能够直接放入字中,也就是说其小于32位,那么咱们就不要在堆上申请空间来存储了,直接把他放到data中就行了。

原始数据的长度小于等于字的长度

data中是存原始数据的指针仍是直接存原始数据取决于原始数据的大小(长度),编译器管理每一个类型的方法列表中的函数,并根据data中是指针仍是原数据做出响应的处理。上面代码中的Binary由于是64位的,因此data存储的是指针,而itable的方法中存储的是(*Binary).String;若是Binary是32位的,那么data中存储的就是原数据,itable中方法列表存储的则是Binary.String

文末总结

  1. 编译过程当中,编译器会为每个类型建立一个类型描述符,该类型描述符包含了该类型的方法集合。
  2. 除了接口类型的其余类型(咱们能够称之为具体类型)和程序中所定义的全部接口类型存在某些转换关系,而当某个具体类型type A能够转换为某个接口类型interface B时,咱们能够认为该具体类型A和该接口类型B存在转换关系,转换关系存储在itable中,该itable对全部的A -> B转换都通用。但由于咱们在程序中可能定义了许许多多的接口口类型于具体类型,因此咱们将"全部的转换关系在编译时预先生成出来"这种方式不可取,一是麻烦,二是会生成不少程序根本就用不到的itable,因此itable在运行时生成。
相关文章
相关标签/搜索