【Free Style】深入Go语言模型(1):interface的底层详解

举报
霜雯 发表于 2017/11/09 10:06:50 2017/11/09
【摘要】 前言Go语言从语法上面来说,是相对简单的,所以基本的语法特性是没有什么特别要讲的,我将从Go语言的一些核心的设计,包括interface,内存模型,defer机制,goroutine的实现与调度,cgo,数组和切片,Go语言的编译器和链接器,GC实现。今天先了解一下Go语言中最引以为傲的特性interface,通过这篇文章我们来了解如下的东西:简要介绍Go语言是如何通过DuckType来实现面向接

image.png



Interface简介

Go语言的Interface和Java里面的Interface与Scala的Traits类似,都是定义一组行为,也就是定义了一个契约:

image.png

上面的代码就是定义了一个Writer的interface,如果要实现这个Interface很简答,只需要实现Write方法就可以了:

type File struct {
    *file // os specific
}

func (f *File) Write(b []byte) (n int, err error) {
    ......
    return n, err
}

同样可以让其他的struct或者类型来实现

type Buffer struct {
    buf       []byte   // contents are the bytes buf[off : len(buf)]
    off       int      // read at &buf[off], write at &buf[len(buf)]
    bootstrap [64]byte // memory to hold first slice; helps small buffers avoid allocation.
    lastRead  readOp   // last read operation, so that Unread*


can

 work correctly. } func (b *Buffer) Write(p []byte) (n int, err error) {     ......     return copy(b.buf[m:], p), nil }

这样File和Buffer都实现了Writer接口,需要注意的是,这种实现是DuckType,只要某个类型实现了Write方法都行,而不需要像Java或者Scala那样必须受Writer类型的影响。(

类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口

然后在需要调用Write的地方,只需要实现这个这样的方法,就可以实现多态的特性,也就是面向接口编程:

func myWrite(w Writer, p []byte)  (n int, err error){     return w.Write(p) }这个函数的第一个参数是Writer类型,那么只需要传入File和Buffer的指针,那么就可以正常工作。


空interface

在Go语言中,有一种interface{}类型,这就就比较像Scala中的Any:

type Any interface {}

对空接口类型实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型。

var any interface{}     any = true     any = 12.34     any = "hello"     any = map[string]int{"one": 1}     any = new(bytes.Buffer)我们可以通过类型断言来进行接口实际类型判断和转换。

Interface的内部实现


非空interface的实现

所有Interface,包括有方法和空接口,在内存中都是占据两个字长。那么在32位机器上就是8个字节,在64位机器上就是16个字节。


##EmptyInterface的底层实现

在Go语言的源码位置: src\runtime\runtime2.go中

type eface struct {
    _type *_type        //类型指针
    data  unsafe.Pointer  //数据区域指针
}

可以看到对于空的interface,其实就是两个指针。,先看第一个rtype类型, 这个就表示类型基本信息,包括类型的大小,对齐信息,类型的编号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 iface struct {     tab  *itab     data unsafe.Pointer } type itab struct {     inter  *interfacetype     _type  *_type     link   *itab     bad    int32     inhash int32      // has this itab been added to hash?     fun    [1]uintptr // variable sized } type interfacetype struct {     typ     _type     pkgpath name     mhdr    []imethod  //接口带有的函数名 }

对于有方法的interface来说,也是两个指针

第一个itab中存放了类型信息,还有一个fun表示方法表。

我们来举一个例子来看看非空接口的是如何表示的:
blobimport (     "strconv"     "fmt" ) 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() {     b := Binary(200)     s := Stringer(b)     fmt.Println(s.String()) }

对于Binary,作为一个64位整数,可以这么表示:
blob

对于s := Stringer(b),可以如下表示:


那么对于s来说

itab中的ityp表示的是Stringer这个接口,typ表示的是Binary这个动态类型,fun函数表中存放的就是Binary中实现了String而接口的方法地址。


对于接口的type-switch,返回的就是静态类型

对于反射里面的TypeOf,返回的是动态类型,也就是数据真实的类型对于调用s.String()方法,其实就是 s.itab->fun[0]。


Interface的赋值过程
blobvar sum uintptr = 0 func sub(inter interface{})  {     t := reflect.TypeOf(inter)     sum += t.Size() } func main()  {     var inter int = 1     sub(inter)     fmt.Println(sum) }

然后生成汇编代码,查看main函数相关的:

MOVQ $1, “”..autotmp_6+48(SP)这个是var inter int = 1

LEAQ type.int(SB), AX 这个是取到了int类型的地址

CALL runtime.convT2E(SB) 这个调用创建了一个interface{}

当我们将一个值(字符串,整数,自定义类型等等Anything)赋给interface{}的时候,Go语言会调用runtime.convT2E去创建Emtpy interface的数据结构,也就是我们前面讲的emptyInterface,这个里面就会有内存申请的动作。(如果要创建是的有方法的interface那么调用的是convT2I方法)


在src/cmd/compile/internal/gc/walk.go中:

会根据interface的源类型和目标类型进行选择对应的函数func convFunc N a me(from, to *Type) string {     tkind := to.iet()     switch from.iet() {     case 'I':         switch tkind {         case 'I':             return "convI2I"         }     case 'T':         switch tkind {         case 'E':             return "convT2E"         case 'I':             return "convT2I"         }     }     Fatalf("unknown conv func %c2%c", from.iet(), to.iet())     panic("unreachable") } func walkexpr(n *Node, init *Nodes) *Node {     case OCONVIFACE:         //完成Interface的转换,包括T2I, T2E, I2I, 这里有一大堆代码,读不太懂,就是需要生成itab或者type信息, 然后调用conv函数生成interface         if isdirectiface(n.Left.Type) {             var t *Node             if n.Type.IsEmptyInterface() {                 t = typename(n.Left.Type)             } else {                 t = itabname(n.Left.Type, n.Type)             }             l := nod(OEFACE, t, n.Left)             l.Type = n.Type             l.Typecheck = n.Typecheck             n = l             break         }         ......         fn := syslook(convFun

***

 me(n.Left.Type, n.Type))
        fn = substArgTypes(fn, n.Left.Type, n.Type)
        dowidth(fn.Type)
        n = nod(OCALL, fn, nil)
}

我们看一下convT2E的代码实现,就是根据参数中type和data生成一个eface:func convT2E(t *_type, elem unsafe.Pointer) (e eface) {     if raceenabled {         raceReadObjectPC(t, elem, getcallerpc(unsafe.Pointer(&t)), funcPC(convT2E))     }     if msanenabled {         msanread(elem, t.size)     }     if isDirectIface(t) {         // This case is implemented directly by the compiler.         throw("direct convT2E")     }     x := newobject(t)     // TODO: We allocate a zeroed object only to overwrite it with     // actual data. Figure out how to avoid zeroing. Also below in convT2I.     typedmemmove(t, x, elem)     e._type = t    //类型赋值     e.data = x     //数据赋值     return }


interface赋值的优化

如果是直接将一个指针类型赋值给interface,那么go语言会比较简单地完成这一次赋值

在go中有这么一个函数,如果是将一个指针那么可以更简单地生成Interface// 

can

 this type be stored directly in an interface word? // Yes, if the representation is a single pointer. func isdirectiface(t *Type) bool {     switch t.Etype {     case TPTR32,         TPTR64,         TCHAN,         TMAP,         TFUNC,         TUNSAFEPTR:         return true     case TARRAY:         // Array of 1 direct iface type 
blob
can
blob

 be direct.         return t.NumElem() == 1 && isdirectiface(t.Elem())     case TSTRUCT:         // Struct with 1 field of direct iface type 


can


 be direct.         return t.NumFields() == 1 && isdirectiface(t.Field(0).Type)     }     return false }



如果赋值给interface的值可以用一个指针的空间来存储,会进行优化吗

我们从前面看到了interface是由一个type和data指针组成的,如果刚好值可以用一个指针空间存储,还需要一个二级指针来存储吗,在网上也有这样的描述:



网上说是会优化的,我实际测试了一下,并没有优化:


输出的结果是类似于:0xc042008240 : 10


所以并没有像网上说的有优化,不知道是不是Go语言就没有实现过这样的优化


Interface的类型断言



我们经常使用type-switch或者v.(T)来判断一个interface是否满足一个另一个interface的要求


b := Binary(200)     var a interface{} = b     if v, ok := a.(Stringer); ok {         fmt.Println(v)     }



从生成的汇编代码可以看到:

CALL runtime.assertE2I2(SB)

上面这个assertE2I2是在runtime\iface.go下面,我们找一个相关的函数看一下


func assertE2I2(inter *interfacetype, e eface) (r iface, b bool) {     t := e._type     if t == nil {         return     }     tab := getitab(inter, t, true)     if tab == nil {         return     }     r.tab = tab     r.data = e.data     b = true     return }blob

上面这个函数式用来判断eface是否是满足一个interfacetype中的接口函数要求。



interface中一个坑的解释

如果Stringer是一个interface,那么

var a interface{} 这个地方用if a == nil是可以判断的

但是如果使用其他类型的nil指针赋给interface


var b *Binary = nil var a interface{} = b p(a)

if a == nil这个时候就不会成立的,即使b为nil,这是因为这个时候a的结构如下:


所以这个时候你把一个其他类型的nil指针赋值给interface{}的时候,interface并不是空,而是对interface进行了初始化,只不过data是nil而已


我们可以从生成的汇编代码来看

LEAQ    type.*"".Binary(SB), AX
MOVQ    AX, (SP)
MOVQ    $0, 8(SP)
PCDATA    $0, $0
CALL    "".p(SB)
上面并没有出现ConvT2E之类的函数调用,但是有一个取type的地址,从上面Interface赋值的过程来看,对于这种nil指针是有优化的。

所有如果一个参数的返回值是interface{}或者其他有方法的interface,比如error等,所以一旦函数返回的是某种interface的时候,就需要注意,不要直接返回某种类型的空指针,需要转换成直接的nil进行赋值。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。