Blog

装箱后的数据与原变量的值再无瓜葛

Content #

经过装箱后,箱内的数据,也就是存放在新分配的内存空间中的数据与原变量便无瓜葛了,比如下面这个例子:

func main() {
  var n int = 61
  var ei interface{} = n
  n = 62  // n的值已经改变
  fmt.Println("data in box:", ei) // 输出仍是61
}

Viewpoint #

From #

staticuint64s区域

Content #

Go 建立了 staticuint64s 区域,对 255 以内的小整数值进行装箱操作时不再分配新内存,而是利用 staticuint64s 区域的内存空间,下面是 staticuint64s 的定义:

// $GOROOT/src/runtime/iface.go
// staticuint64s is used to avoid allocating in convTx for small integer values.
var staticuint64s = [...]uint64{
    0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
    0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
  ... ...
}

Viewpoint #

From #

接口类型的装箱(boxing)原理

Content #

接口类型有着复杂的内部结构,所以我们将一个类型变量值赋值给一个接口类型变量值的过程肯定不会像var i int = 5那么简单,那么接口类型变量赋值的过程是怎样的呢?其实接口类型变量赋值是一个“装箱”的过程。

装箱(boxing)是编程语言领域的一个基础概念,一般是指把一个值类型转换成引用类型,比如在支持装箱概念的 Java 语言中,将一个 int 变量转换成 Integer 对象就是一个装箱操作。

在 Go 语言中,将任意类型赋值给一个接口类型变量也是装箱操作。接口类型的装箱实际就是创建一个 eface 或 iface 的过程。

// interface_internal.go
    type T struct {
        n int
        s string
    }
    func (T) M1() {}
    func (T) M2() {}
    type NonEmptyInterface interface {
        M1()
        M2()
    }

    func main() {
        var t = T{
            n: 17,
            s: "hello, interface",
        }
        var ei interface{}
        ei = t

        var i NonEmptyInterface
        i = t
        fmt.Println(ei)
        fmt.Println(i)
    }

对 ei 和 i 两个接口类型变量的赋值都会触发装箱操作,要想知道 Go 在背后做了些什么,我们需要“下沉”一层,也就是要输出上面 Go 代码对应的汇编代码:

...

空接口类型变量与非空接口类型变量的等值比较

空接口类型变量与非空接口类型变量的等值比较 #

下面是非空接口类型变量和空接口类型变量之间进行比较的例子:

func printEmptyInterfaceAndNonEmptyInterface() {
var eif interface{} = T(5)
var err error = T(5)
println("eif:", eif)
println("err:", err)
println("eif = err:", eif == err)

err = T(6)
println("eif:", eif)
println("err:", err)
println("eif = err:", eif == err)
}

这个示例的输出结果如下:

eif: (0x10b3b00,0x10eb4d0)
err: (0x10ed380,0x10eb4d8)
eif = err: true
eif: (0x10b3b00,0x10eb4d0)
err: (0x10ed380,0x10eb4e0)
eif = err: false

空接口类型变量和非空接口类型变量内部表示的结构有所不同(第一个字段:_type vs. tab),两者似乎一定不能相等。但 Go 在进行等值比较时,类型比较使用的是 eface 的 _type 和 iface 的 tab._type,因此就像我们在这个例子中看到的那样,当 eif 和 err 都被赋值为T(5)时,两者之间是划等号的。

Viewpoint #

From #

29|接口:为什么nil接口不等于nil?

iface结构

Content #

// $GOROOT/src/runtime/runtime2.go
type iface struct {
    tab  *itab
    data unsafe.Pointer
}

iface 用于表示非空的拥有方法的接口 interface 类型变量。

iface 除了要存储动态类型信息之外,还要存储接口本身的信息(接口的类型信息、方法列表信息等)以及动态类型所实现的方法的信息,因此 iface 的第一个字段指向一个itab类型结构。itab 结构的定义如下:

// $GOROOT/src/runtime/runtime2.go
type itab struct {
    inter *interfacetype
    _type *_type
    hash  uint32 // copy of _type.hash. Used for type switches.
    _     [4]byte
    fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

itab 结构中的字段_type(参看 eface结构)则存储着这个接口类型变量的动态类型的信息。字段fun则是动态类型已实现的接口方法的调用地址数组。 itab 结构中的第一个字段inter指向的 interfacetype 结构,存储着这个接口类型自身的信息。interfacetype 结构由类型信息(typ)、包路径名(pkgpath)和接口方法集合切片(mhdr)组成。

// $GOROOT/src/runtime/type.go
type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod
}

我们来看一个用 iface 表示非空接口类型变量的例子:

type T struct {
    n int
    s string
}
func (T) M1() {}
func (T) M2() {}
type NonEmptyInterface interface {
    M1()
    M2()
}
func main() {
    var t = T{
        n: 18,
        s: "hello, interface",
    }
    var i NonEmptyInterface = t
}

和 eface 比起来,iface 的表示稍微复杂些。我也画了一幅表示上面 NonEmptyInterface 接口类型变量在 Go 运行时表示的示意图:

...

eface结构

Content #

// $GOROOT/src/runtime/runtime2.go
type eface struct {
    _type *_type
    data  unsafe.Pointer
}

eface 用于表示没有方法的空接口(empty interface)类型变量,也就是 interface{}类型的变量。

_type结构为该接口类型变量的动态类型的信息,它的定义是这样的:

// $GOROOT/src/runtime/type.go
type _type struct {
    size       uintptr
    ptrdata    uintptr // size of memory prefix holding all pointers
    hash       uint32
    tflag      tflag
    align      uint8
    fieldAlign uint8
    kind       uint8
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    equal func(unsafe.Pointer, unsafe.Pointer) bool
    // 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
}

我们看一个用 eface 表示的空接口类型变量的例子:

...

人类曾经处于灭绝的边缘

人类也曾经也处于灭绝的边缘,这件事发生在什么时候?是怎么发生的? #

最近一项由人类基因学家对DNA(脱氧核糖核酸)的分析发现,大约在14万年前左右,一场灾难——或许由气候变化引发——导致大批现代人属物种死亡,他们绝大多数生活在当时的非洲。在那一时期,人类亚种的数量锐减至只有数百人——我们成为今天所谓的“濒危动物”,就像是山地大猩猩或者蓝鲸。艾萨克·牛顿、阿尔伯特·爱因斯坦以及其他你曾经听说过的人,还有生活在今天这个世界上的数以亿计的人,都是这几百个存活下来的智人的后裔。

Viewpoint #

From #

原型系统的两条基本原则

JavaScript中的原型系统可用哪两条基本原则来概括? #

  1. 如果所有对象都有私有字段\[\[prototype\]\]就是对象的原型;
  2. 读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止。

Viewpoint #

From #

mcache的结构

mcache的结构 #

mcache 是 Go 的线程缓存,对应于 TCMalloc 中的 Thread cache 结构。 mcache 会与线程绑定,每个 goroutine 在向 mcache 申请内存时,都不会与其他 goroutine 发生竞争。mcache 中会维护上述 68×2 种 spanClass 的 mspan 数组,存放在 mcache 的 alloc 中,包括 scan 以及 noscan 两个队列。 mcache 的主要结构如下:

type mcache struct {
    ...
    tiny       uintptr
    tinyoffset uintptr
    tinyAllocs uintptr

    alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass
    ...
}

当 mcache 中的内存不够需要扩容时,需要向 mcentral 请求,mcentral 对应于 TCMalloc 中的 Central cache 结构。

Viewpoint #

From #

24 | GC实例:Python和Go的内存管理机制是怎样的?

...

iface与eface

iface与eface #

接口类型“动静兼备”的特性也决定了它的变量的内部表示绝不像一个静态类型变量(如 int、float64)那样简单,我们可以在 $GOROOT/src/runtime/runtime2.go中找到接口类型变量在运行时的表示:

// $GOROOT/src/runtime/runtime2.go
type iface struct {
    tab  *itab
    data unsafe.Pointer
}
type eface struct {
    _type *_type
    data  unsafe.Pointer
}

这两种表示分别用于不同的接口类型变量:

  1. eface结构 用于表示没有方法的空接口(empty interface)类型变量,也就是 interface{}类型的变量;
  2. iface结构 用于表示其余拥有方法的接口 interface 类型变量。

这两个结构的共同点是它们都有两个指针字段,并且第二个指针字段的功能相同,都是指向当前赋值给该接口类型变量的动态类型变量的值。

它们的不同点在于 eface 表示的空接口类型并没有方法列表,因此它的第一个指针字段指向一个_type类型结构。

而 iface 除了要存储动态类型信息之外,还要存储接口本身的信息(接口的类型信息、方法列表信息等)以及动态类型所实现的方法的信息,因此 iface 的第一个字段指向一个itab类型结构。

每个接口类型变量在运行时的表示都是由两部分组成的,针对不同接口类型我们可以简化记作:eface(_type, data)和iface(tab, data)。

Viewpoint #

From #

29|接口:为什么nil接口不等于nil?