Golang中的Interface详解

背景:

golang的interface是一种satisfied式的。A类只要实现了IA interface定义的方法,A就satisfied了接口IA。更抽象一层,如果某些设计上需要一些更抽象的共性,比如print各类型,这时需要使用reflect机制,reflect实质上就是将interface的实现暴露了一部分给应用代码。要理解reflect,需要深入了解interface。go的interface是一种隐式的interface,但golang的类型是编译阶段定的,是static的,如:

type MyInt int
var i int
var j MyInt

虽然MyInt底层就是int,但在编译器角度看,i的类型是int,j的类型是MyInt,是静态、不一致的。两者要赋值必须要进行类型转换。即使是interface,就语言角度来看也是静态的。如:

var r io.Reader

不管r后面用什么来初始化,它的类型总是io.Reader。更进一步,对于空的interface,也是如此。记住go语言类型是静态这一点,对于理解interface/reflect很重要。看一例:

var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}
r = tty

到这里,r的类型是什么?r的类型仍然是interface io.Reader,只是r = tty这一句,隐含了一个类型转换,将tty转成了io.Reader。

interface的实现:

作为一门编程语言,对方法的处理一般分为两种类型:一是将所有方法组织在一个表格里,静态地调用(C++, java);二是调用时动态查找方法(python, smalltalk, js)。而go语言是两者的结合:虽然有table,但是是需要在运行时计算的table。如下例:Binary类实现了两个方法,String()和Get()

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

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

因为它实现了String(),按照golang的隐式方法实现来看,Binary satisfied了Stringer接口。因此它可以赋值: s:=Stringer(b)。以此为例来说明下interface的实现:interface的内存组织如图:

一个interface值由两个指针组成,第一个指向一个interface table,叫 itable。itable开头是一些描述类型的元字段,后面是一串方法。注意这个方法是interface本身的方法,并非其dynamic value(Binary)的方法。即这里只有String()方法,而没有Get方法。但这个方法的实现肯定是具体类的方法,这里就是Binary的方法。
当这个interface无方法时,itable可以省略,直接指向一个type即可。
另一个指针data指向dynamic value的一个拷贝,这里则是b的一份拷贝。也就是,给interface赋值时,会在堆上分配内存,用于存放拷贝的值。
同样,当值本身只有一个字长时,这个指针也可以省略。
一个interface的初始值是两个nil。比如,

var w io.Writer

这时,tab和data都是nil。interface是否为nil取决于itable字段。所以不一定data为nil就是nil,判断时要额外注意。

这样,像这样的代码:

switch v := any.(type) {
case int:
    return strconv.Itoa(v)
case float:
    return strconv.Ftoa(v, 'g', -1)
}

实际上是any这个interface取了  any. tab->type。

而interface的函数调用实际上就变成了:

s.tab->fun[0](s.data)。第一个参数即自身类型指针。

itable的生成:

itable的生成是理解interface的关键。

如刚开始处提的,为了支持go语言这种接口间仅通过方法来联系的特性,是没有办法像C++一样,在编译时预先生成一个method table的,只能在运行时生成。因此,自然的,所有的实体类型都必须有一个包含此类型所有方法的“类型描述符”(type description structure);而interface类型也同样有一个类似的描述符,包含了所有的方法。

这样,interface赋值时,计算interface对象的itable时,需要对两种类型的方法列表进行遍历对比。如后面代码所示,这种计算只需要进行一次,而且优化成了O(m+n)。

可见,interface与itable之间的关系不是独立的,而是与interface具体的value类型有关。即(interface类型, 具体类型)->itable。

var any interface{}  // initialized elsewhere
s := any.(Stringer)  // dynamic conversion
for i := 0; i < 100; i++ {
    fmt.Println(s.String())
}

itable的计算不需要到函数调用时进行,只需要在interface赋值时进行即可,如上第2行,不需要在第4行进行。

最后,看一些实现代码:

以下是上面图中的两个字段。

type iface struct {
    tab  *itab     // 指南itable
    data unsafe.Pointer     // 指向真实数据
}

再看itab的实现:

type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    bad    int32
    unused int32
    fun    [1]uintptr // variable sized
}

可见,它使用一个疑似链表的东西,可以猜这是用作hash表的拉链。前两个字段应该是用来表达具体的interface类型和实际拥有的值的类型的,即一个itable的key。(上文提到的(interface类型, 具体类型) )

type imethod struct {
    name nameOff
    ityp typeOff
}

type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod
}

interfacetype如有若干imethod,可以猜想这是表达interface定义的方法数据结构。

type _type struct {
    size       uintptr
    ptrdata    uintptr // size of memory prefix holding all pointers
    hash       uint32
    tflag      tflag
    align      uint8
    fieldalign uint8
    kind       uint8
    alg        *typeAlg
    // gcdata stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, gcdata is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}

对于_type,可见里面有gc的东西,应该就是具体的类型了。这里有个hash字段,itable实现就是挂在一个全局的hash table中。hash时用到了这个字段:

func itabhash(inter *interfacetype, typ *_type) uint32 {
    // compiler has provided some good hash codes for us.
    h := inter.typ.hash
    h += 17 * typ.hash
    // TODO(rsc): h += 23 * x.mhash ?
    return h % hashSize
}

可见,这里有个把interface类型与具体类型之间的信息结合起来做一个hash的过程,这个hash就是上述的itab的存储地点,itab中的link就是hash中的拉链。

回到itab,看取一个itab的逻辑:

如果发生了typeassert或是interface的赋值(强转),需要临时计算一个itab。这时会先在hash表中找,找不到才会真实计算。

     h := itabhash(inter, typ)

     // look twice - once without lock, once with.
     // common case will be no lock contention.
     var m *itab
     var locked int
     for locked = 0; locked < 2; locked++ {
         if locked != 0 {
             lock(&ifaceLock)
         }
         for m = (*itab)(atomic.Loadp(unsafe.Pointer(&hash[h]))); m != nil; m = m.link {
             if m.inter == inter && m._type == typ {
                 return m    // 找到了前面计算过的itab
             }
         }
     }
    // 没有找到,生成一个,并加入到itab的hash中。
     m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
     m.inter = inter
     m._type = typ
     additab(m, true, canfail)

这个hash是个全局变量:

 const (
     hashSize = 1009
 )

 var (
     ifaceLock mutex // lock for accessing hash
     hash      [hashSize]*itab
 )

最后,看一下如何生成itab:

     // both inter and typ have method sorted by name,
     // and interface names are unique,
     // so can iterate over both in lock step;
     // the loop is O(ni+nt) not O(ni*nt).       // 按name排序过的,因此这里的匹配只需要O(ni+nt)
     j := 0
     for k := 0; k < ni; k++ {
         i := &inter.mhdr[k]
         itype := inter.typ.typeOff(i.ityp)
         name := inter.typ.nameOff(i.name)
         iname := name.name()
         for ; j < nt; j++ {
             t := &xmhdr[j]
             tname := typ.nameOff(t.name)
             if typ.typeOff(t.mtyp) == itype && tname.name() == iname {
                     if m != nil {
                         ifn := typ.textOff(t.ifn)
                         *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn // 找到匹配,将实际类型的方法填入itab的fun
                     }
                     goto nextimethod
                 }
             }
         }
     nextimethod:
     }
     h := itabhash(inter, typ)             //插入上面的全局hash
     m.link = hash[h]
     atomicstorep(unsafe.Pointer(&hash[h]), unsafe.Pointer(m))
 }

到这里,interface的数据结构的框架。

reflection实质上是将interface背后的实现暴露了一部分给应用代码,使应用程序可以使用interface实现的一些内容。只要理解了interface的实现,reflect就好理解了。如reflect.typeof(i)返回interface i的type,Valueof返回value。

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

(0)

相关推荐

  • golang中interface接口的深度解析

    一 接口介绍 如果说gorountine和channel是支撑起Go语言的并发模型的基石,让Go语言在如今集群化与多核化的时代成为一道亮丽的风景,那么接口是Go语言整个类型系列的基石,让Go语言在基础编程哲学的探索上达到前所未有的高度.Go语言在编程哲学上是变革派,而不是改良派.这不是因为Go语言有gorountine和channel,而更重要的是因为Go语言的类型系统,更是因为Go语言的接口.Go语言的编程哲学因为有接口而趋于完美.C++,Java 使用"侵入式"接口,主要表现在实现

  • 详解Golang语言中的interface

    interface是一组method签名的组合,interface可以被任意对象实现,一个对象也可以实现多个interface.任意类型都实现了空interface(也就是包含0个method的interface),空interface可以存储任意类型的值.interface定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口. go version go1.12 package main import ( "fmt" ) // 定义struct type Hu

  • 浅谈Golang 嵌套 interface 的赋值问题

    大家还是直接看代码吧~ package main import ( "fmt" ) func main() { s := map[string]interface{}{ "code":0, "msg":"", "data":map[string]interface{}{ "src":"", }, } s["data"].(map[string]in

  • 解决golang 反射interface{}做零值判断的一个重大坑

    在对float零值判断时往往只需要和0做==即可,所以曾经int和float都用==0来做对比, 比如下方: in := 0. var tmp interface{} = float32(in) fmt.Println("float 0==0:", in == 0) fmt.Println("float -> interface{} -> float", tmp.(float32) == 0) switch v := tmp.(type) { case

  • 深入Golang的接口interface

    目录 前言 接口转换的原理 实现多态 前言 go不要求类型显示地声明实现了哪个接口,只要实现了相关的方法即可,编译器就能检测到 空接口类型可以接收任意类型的数据: type eface struct { // _type 指向接口的动态类型元数据 // 描述了实体类型.包括内存对齐方式.大小等 _type *_type // data 指向接口的动态值 data unsafe.Pointer } 空接口在赋值时,_type 和 data 都是nil.赋值后,_type 会指向赋值的数据元类型,d

  • golang基础之Interface接口的使用

    接口是一个或多个方法签名名的集合,定义方式如下 type Interface_Name interface { method_a() string method_b() int .... } 只要某个类型拥有该接口的所有方法签名,就算实现该接口,无需显示声明实现了那个接口,这称为structural Typing package main import "fmt" type USB interface { //定义一个接口:方法的集合 Name() string //Name方法,返回

  • golang中struct和interface的基础使用教程

    前言 本文主要给大家介绍了关于golang中struct和interface的相关内容,是属于golang的基本知识,下面话不多说了,来一起看看详细的介绍吧. struct struct 用来自定义复杂数据结构,可以包含多个字段(属性),可以嵌套:go中的struct类型理解为类,可以定义方法,和函数定义有些许区别:struct类型是值类型. struct定义 type User struct { Name string Age int32 mess string } var user User

  • Golang中interface{}转为数组的操作

    interface{} 转为普通类型 我们都知道在golang中interface{}可以代表任何类型,对于像int64.bool.string等这些简单类型,interface{}类型转为这些简单类型时,直接使用 p, ok := t.(bool) p, ok := t.(int64) 如果ok==true的话,就已经类型转换成功. 假设有这样一个场景,我们有一个函数有返回值,但是返回值的类型不定,所以我们的返回值类型只能以接口来代替了. 返回接口类型之后,我们就要对其类型进行判断然后进行类型

  • Golang中的Interface详解

    背景: golang的interface是一种satisfied式的.A类只要实现了IA interface定义的方法,A就satisfied了接口IA.更抽象一层,如果某些设计上需要一些更抽象的共性,比如print各类型,这时需要使用reflect机制,reflect实质上就是将interface的实现暴露了一部分给应用代码.要理解reflect,需要深入了解interface.go的interface是一种隐式的interface,但golang的类型是编译阶段定的,是static的,如:

  • Go语言中你不知道的Interface详解

    前言 最近在看Go语言的面向对象的知识点时,发现它的面向对象能力全靠 interface 撑着,而且它的 interface 还与我们以前知道的 interface 完全不同.故而整个过程不断的思考为什么要如此设计?这样设计给我们带来了什么影响? interface 我不懂你 Rob Pike 曾说: 如果只能选择一个Go语言的特 性移植到其他语言中,他会选择接口 被Go语言设计者如此看重,想来 interface 一定是资质不凡,颜值爆表.但是说实话,当我第一次读这部分内容的时候,我产生了以下

  • Golang中异常处理机制详解

    前言 通常我们需要编写好的错误处理方式,在了避免某些程序员滥用异常,于是Go这里直接把异常这一块给砍掉了,最终还是通过返回值来判断程序的异常情况,毕竟Go可是支持多返回值的语言,比如atoi.itoa等函数,就不能忽略它的第二个返回值,因为第二个返回值代表了转换是否成功!不过Golang还是提供了一些错误处理机制的 Go的错误机制 1.没有异常机制 2.error类型实现了error接口 3.可以通过errors.New来快速创建错误实例 type error interface{ Error(

  • Golang标准库syscall详解(什么是系统调用)

    一.什么是系统调用 In computing, a system call is the programmatic way in which a computer program requests a service from the kernel of the operating system it is executed on. This may include hardware-related services (for example, accessing a hard disk dri

  • Golang 动态脚本调研详解

    目录 一.技术背景 1.1 程序的动态链接技术 1.1.1 动态链接库 1.1.2 动态共享对象 1.1.3 非编译语言的动态技术 1.2 Golang 的动态技术 二.Golang 的第三方解释器(Yaegi) 2.1 使用场景 2.1.1 内嵌解释器 2.1.2 动态扩展框架 2.1.3 命令行解释器 2.2 数据交互 2.2.1 数据输入 2.1.2 数据输出 三.实现原理 3.1 AST - 抽象语法树 3.1.1 抽象语法树示例 3.1.2 执行抽象语法树 一.技术背景 1.1 程序的

  • Golang接口使用教程详解

    目录 前言 一.概述 二.接口类型 2.1 接口的定义 2.2 实现接口的条件 2.3 为什么需要接口 2.4 接口类型变量 三.值接收者和指针接收者 3.1 值接收者实现接口 3.2 指针接收者实现接口 四.类型与接口的关系 4.1 一个类型实现多个接口 4.2 多种类型实现同一接口 五.接口嵌套 六.空接口 七.类型断言 总结 前言 go语言并没有面向对象的相关概念,go语言提到的接口和java.c++等语言提到的接口不同,它不会显示的说明实现了接口,没有继承.子类.implements关键

  • golang之log rotate详解

    操作系统: CentOS 6.9_x64 go语言版本: 1.8.3 问题描述 golang的log模块提供的有写日志功能,示例代码如下: /* golang log example */ package main import ( "log" "os" ) func main() { logFile,err := os.Create("test1.log") defer logFile.Close() if err != nil { log.F

  • Golang与python线程详解及简单实例

    Golang与python线程详解及简单实例 在GO中,开启15个线程,每个线程把全局变量遍历增加100000次,因此预测结果是 15*100000=1500000. var sum int var cccc int var m *sync.Mutex func Count1(i int, ch chan int) { for j := 0; j < 100000; j++ { cccc = cccc + 1 } ch <- cccc } func main() { m = new(sync.

  • Dubbo在Spring和Spring Boot中的使用详解

    一.在Spring中使用Dubbo 1.Maven依赖 <dependency> <groupId>com.alibaba</groupId> <artifactId>dubbo</artifactId> <version>2.5.3.6</version> <exclusions> <exclusion> <groupId>log4j</groupId> <artif

  • c# 实现控件(ocx)中的事件详解

    c#控件实现类似c++中ocx控件功能 c++中ocx控件 1.控件方法 2.控件事件 c#很容易实现c++中ocx中控件方法的功能,但是实现类似c++中ocx的控件事件,则需要一定的周折. 下面就用实例简单的介绍c#如何实现 c#中ActiveX(ocx)实现实例(vs2008环境下): using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using

随机推荐