前言

Go语言从语法上面来说,是相对简单的,所以基本的语法特性是没有什么特别要讲的,这里将从Go语言的一些核心的设计,包括interface,内存模型,defer机制,goroutine的实现与调度,cgo,数组和切片,Go语言的编译器和链接器,GC实现。

先了解一下Go语言中最引以为傲的特性interface,通过这篇文章来了解如下的东西:

  • 简要介绍Go语言是如何通过DuckType来实现面向接口编程
  • 底层是如何实现空的interface和有方法的interface
  • interface底层的内存模型
  • interface赋值时的转换过程
  • interface类型断言的实现

    注:源码分析都是基于Go1.8和Windows7平台

interface简介

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

type Writer interface {
    Write(p []byte) (n int, err error)
}

上面的代码就是定义了一个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{}类型

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个字节。

空interface的底层实现

在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
}

非空interface的底层实现

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表示方法表。
举一个例子来看看非空接口的是如何表示的:

import (
    "fmt"
    "strconv"
)

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),可以如下表示:
blob

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

对于接口的type-switch,返回的就是静态类型
对于反射里面的TypeOf,返回的是动态类型,也就是数据真实的类型

对于调用s.String()方法,其实就是 s.itab->fun[0]。

interface的赋值过程

var 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)
LEAQ   type.int(SB), AX
MOVQ   AX, (SP)
LEAQ   ""..autotmp_6+48(SP), AX
MOVQ   AX, 8(SP)
PCDATA $0, $0
CALL   runtime.convT2E(SB)
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的数据结构,也就是前面讲的空interface,这个里面就会有内存申请的动作。(如果要创建是的有方法的interface那么调用的是convT2I方法)

在src/cmd/compile/internal/gc/walk.go中:
会根据interface的源类型和目标类型进行选择对应的函数

func convFuncName(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(convFuncName(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 can 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指针组成的,如果刚好值可以用一个指针空间存储,还需要一个二级指针来存储吗,在网上也有这样的描述:
blob
网上说是会优化的,我实际测试了一下,并没有优化:

type eface interface{
	_type unsafe.Pointer
	data  unsafe.Pointer
}

func main(){
	v := 10
	var a interface{} = v
	
	var f eface = *(*eface)(unsafe.Pointer(&a))
	fmt.Println(f.data, ":", *(*int)(f.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
}

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

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的结构如下:

*Binary
nil

所以这个时候你把一个其他类型的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进行赋值。

总结

上面也仅仅是简单了分析了一些interface的底层机制,还有很多细节的东西没有分析,但是已经把整个interface的内存模型,赋值过程分析了一些,相信可以帮助大家理解进一步interface。如果这个过程中有什么错误的地方,也希望大家指正一下 :)

在reflect包中也有两个定义, 暂时还不清楚这个两个interface和前面的iface与eface的区别,在itab的定义上面有点差异,后面研究反射的时候再看看:

// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
    typ  *rtype               //类型指针
    word unsafe.Pointer       //值
}
// nonEmptyInterface is the header for a interface value with methods.
type nonEmptyInterface struct {
    // see ../runtime/iface.go:/Itab
    itab *struct {
        ityp   *rtype // static interface type
        typ    *rtype // dynamic concrete type
        link   unsafe.Pointer
        bad    int32
        unused int32
        fun    [100000]unsafe.Pointer // method table
    }
    word unsafe.Pointer
}