原文 Go Data Structures: Interfaces

作者 Russ Cox

声明:本文目的仅仅作为个人mark,所以在翻译的过程中参杂了自己的思想甚至改变了部分内容。但由于译者水平有限,所写文字或者代码可能会误导读者,如发现文章有问题,请尽快告知,不胜感激。


一些知识点

  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语句则会在结束前判断其是否为几个基本类型然后执行响应的逻辑。

实现一个简单的例子,有一个新的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位长的类型

接口类型的值表现为两个字(假设我们处于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在运行时生成。

[译] Go数据结构-接口的更多相关文章

  1. go中的数据结构接口-interface

    1. 接口的基本使用 golang中的interface本身也是一种类型,它代表的是一个方法的集合.任何类型只要实现了接口中声明的所有方法,那么该类就实现了该接口.与其他语言不同,golang并不需要 ...

  2. 常用数据结构及复杂度 array、LinkedList、List、Stack、Queue、Dictionary、SortedDictionary、HashSet、SortedSet

    原文地址:http://www.cnblogs.com/gaochundong/p/data_structures_and_asymptotic_analysis.html  常用数据结构的时间复杂度 ...

  3. socket , 套接口还是套接字,傻傻分不清楚

    socket 做网络通信的朋友大都对socket这个词不会感到陌生,但是它的中文翻译是叫套接口还是套接字呢,未必大多数朋友能够分清,今天我们就来聊聊socket的中文名称. socket一词的起源 在 ...

  4. 从壹开始 [ Nuxt.js ] 之二 || 项目搭建 与 接口API

    前言 哈喽大家周一好,今天的内容比较多,主要就是包括:把前端页面的展示页给搭出来,然后调通接口API,可以添加数据,这两天我也一直在开发,本来想一篇一篇的写,发现可能会比较简单,就索性把项目搭建的过程 ...

  5. app 调用接口

    app 调用接口 /// <summary> /// 是否跳转到活动注册成功页面 /// </summary> /// <returns></returns& ...

  6. 数据结构之LinkList

    1.结构: 2.Link代码: public class Link { public int iData; public double dData; public Link next; public ...

  7. javascript 数据结构与算法---二叉数

    二叉树,首先了解一些关于二叉数的概念(来自百度百科) 1. 二叉树(Binary tree)是树形结构的一个重要类型 2. 定义: 二叉树(binary tree)是指树中节点的度不大于2的有序树,它 ...

  8. FFmpeg libavutil主要功能概述

    [时间:2017-08] [状态:Open] [关键词:ffmpeg,avutil,avrational,avlog,avbuffer,avoptoin] 0 引言 FFmpeg使用很久了,一直没有认 ...

  9. 优先队列 :Priority Queue

    PriorityQueue是从JDK1.5开始提供的新的数据结构接口,它是一种基于优先级堆的极大优先级队列.优先级队列是不同于先进先出队列的另一种队列. 每次从队列中取出的是具有最高优先权的元素.如果 ...

随机推荐

  1. Confluence 6 导入一个 Confluence 站点

    有下面 2 种类方法可以导入一个站点 - 通过上传一个文件或者从你 Confluence 服务器上读取一个目录.上传文件仅仅是针对一个小站点的情况.为了取得最好的导入结果,我们推荐你从服务器上的目录上 ...

  2. jQuery获取地址栏中的链接参数

    http://caibaojian.com/177.html 问题描述 今天做一个主题,有一个需求是根据不同的页面来做,虽然php也可以做到,不过考虑到自己的特效代码都是在jQuery上完成,想着能否 ...

  3. 【深度学习】吴恩达网易公开课练习(class1 week3)

    知识点梳理 python工具使用: sklearn: 数据挖掘,数据分析工具,内置logistic回归 matplotlib: 做图工具,可绘制等高线等 绘制散点图: plt.scatter(X[0, ...

  4. 单例、异常、eval函数

    一.单例 01. 单例设计模式 设计模式 设计模式 是 前人工作的总结和提炼,通常,被人们广泛流传的设计模式都是针对 某一特定问题 的成熟的解决方案 使用 设计模式 是为了可重用代码.让代码更容易被他 ...

  5. Unity3D用户手册

    Unity Manual 用户手册 Welcome to Unity. 欢迎使用Unity. Unity is made to empower users to create the best int ...

  6. openstack 基础

    一:openstack起源: 1.rackspace和NASA(美国国家航空航天局)共同发起的开源项目 1.1/rackspace:贡献的swaft子项目(存储组件) 1.2/NASA:贡献了nova ...

  7. 学习笔记: 反射应用、原理,完成扩展,emit动态代码

    using Ruanmou.DB.Interface; using Ruanmou.DB.MySql; using Ruanmou.DB.SqlServer; using Ruanmou.Model; ...

  8. 【转】Android逆向入门流程

    原文:https://www.jianshu.com/p/71fb7ccc05ff 0.写在前面 本文是笔者自学笔记,以破解某目标apk的方式进行学习,中间辅以原理性知识,方便面试需求. 参考文章的原 ...

  9. javaScript事件(九)事件类型之触摸与手势事件

    一.触摸事件 touchstart:当手指触摸屏幕时触发:即使已经有一个手指放在了屏幕上也会触发. touchmove:当手指在屏幕上滑动时连续地触发.在这个世界发生期间,调用preventDefau ...

  10. Java基础知识➣泛型整理(四)

    概述 泛型的本质是参数化类型,使用同一套代码来满足不同数据类型的业务需要,提高代码的执行效率,使代码简单明了. 泛型方法 该方法在调用时可以接收不同类型的参数.根据传递给泛型方法的参数类型,编译器适当 ...