20 Nov 2018

golang数据类型的实现机制

内容

golang基本数据类型

  命令式程序设计(C/C++/JAVA/GO/PYTHON/…)通过改变变量的状态来完成计算过程。变量本质上是对应一个内存地址,内存地址中存储的bit称为变量的值。而如何解析一个地址中所存储的bit,则是由该变量对应的数据类型决定。比如在C语言中,int变量的32个bit被联结起来解析为一个整型数。

  golang也是一门传统的命令式编程语言,因此它具有和其他常用语言相似的语法,同样的,它也实现了类似于其他语言的变量数据类型。基本的数据类型包括8~64位的uint和int,以及float32,float64等,这些和其他语言没有什么本质的区别。golang当然也实现了string类型,不过string类型和C/C++的string还是有很大的不同,它做了一些封装。除此之外,C/C++中以标准库的形式提供的动态数组、哈希表等,golang也是以基本数据类型提供给了用户(slice,map)。

  golang还提供了类似C中void的类型-interface,并基于interface实现了类型反射机制。本文后面还将深入源码了解interface是如何实现void的万能性,以及在此基础上的reflect实现机制。

  目标:本文的目标是从源码层面深入理解golang类型实现机制,主要包括各种类型在runtime中的表现形式,以及类型实现的一些周边,包括反射机制等。

  方法:反汇编 + 阅读go开源代码

类型的runtime实现

  所谓类型的的runtime实现,指的是类型在程序运行时的底层表现形式。在golang中,所有类型的实现结构体都包含一个统一的头信息:

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
}

结构体包含了类型在运行时的一些元信息,包括类型名称,GC相关等,主要是一些管理信息。在_type的基础上,golang定义了interfacetype,maptype,arraytype,chantype,slicetype,functype,ptrtype,structtype。具体这些信息就比较显然,就不贴代码了。

  这里,我们关心的是golang在编译期间会分别为这些不同的类型结构体注入哪些信息,以及runtime提供了哪些接口可以获得类型的对应哪些相关的信息。对于第一点,它代表了用户程序所能运用的类型信息的极限,对于第二点,则提供了使用方法。当然这两点是相辅相成的,所以下面我们深入源码看一下runtime中提供的对外获取类型信息的接口,同时基于此了解runtime的类型结构到底包含了哪些信息,以及是如何包含的。一般来说,语言的类型系统主要是为了编译检查,保证程序的稳定性和安全性,用户不需要过多干预语言的类型信息,因此golang对应的接口也比较简单。

  观察type.go,_type实现的函数主要包括以下几个:

func (t *_type) string() string
func (t *_type) uncommon() *uncommontype
func (t *_type) name() string
func (t *_type) pkgpath() string
func (t *_type) nameOff(off nameOff) name
func (t *_type) typeOff(off typeOff) *_type
func (t *_type) textOff(off textOff) unsafe.Pointer

其中几个*off函数主要是获取类型在module中的相关信息,比如类型名,包路径等。这里的module指的是定义该类型的模块。这些函数都是根据信息在module镜像文件中的偏移量来获取信息的。

这里比较重要一点的应该是uncommon函数,这个函数提供了根据类型头信息获取类型结构体具体信息的入口。在_type结构体中有一个tflag字段,这个字段告诉外界是否可以通过直接跨越类型头信息的内存区域读取该类型的其他信息。举例来说,对于一个map类型,我们可以通过tflag知道在头部maptype结构体之后是否还存储了该map类型的其他信息,比如该类型实现的method信息等:

func (t *_type) uncommon() *uncommontype { 
	if t.tflag&tflagUncommon == 0 {
		return nil
    }
    ....
    	case kindMap:
		type u struct {
			maptype
			u uncommontype
		}
        return &(*u)(unsafe.Pointer(t)).u
    ....
type uncommontype struct {
	pkgpath nameOff
	mcount  uint16 // number of methods
	xcount  uint16 // number of exported methods
	moff    uint32 // offset from this uncommontype to [mcount]method
	_       uint32 // unused
}

主要是包含method信息(exported or not)。总而言之,golang在runtime为各个类型构造了类型模板。程序可以根据这些类型模板获取类型信息。

类型与实例

类型是一个模板,只有真正被程序使用才会创建实例。上一节介绍的就是类型的模板,表现形式位为结构体。我们可以通过程序来验证这一点:

package main

import "reflect"
import "fmt"

func main(){
    var m = make(map[int]int)  \\1
    var n = make(map[int]int)  \\2
    m[1] = 1
    n[1] = 2
    var a interface{}
    var c interface{}
    a = m
    b := reflect.TypeOf(a)

    c = n
    d := reflect.TypeOf(c)
    fmt.Printf("typename:%v,&v\n",b,d)
}

上面这段简单的代码片段对应的汇编如下(部分):

....
  test.go:6		0x1092987		488dac2490000000	LEAQ 0x90(SP), BP
  test.go:7		0x109298f		e84c8df7ff		CALL runtime.makemap_small(SB)
  test.go:7		0x1092994		488b0424		MOVQ 0(SP), AX
  test.go:7		0x1092998		4889442448		MOVQ AX, 0x48(SP)
  test.go:8		0x109299d		e83e8df7ff		CALL runtime.makemap_small(SB)
  test.go:8		0x10929a2		488b0424		MOVQ 0(SP), AX
  test.go:8		0x10929a6		4889442440		MOVQ AX, 0x40(SP)
  test.go:9		0x10929ab		488d0d6e6b0100		LEAQ runtime.rodata+92288(SB), CX
  test.go:9		0x10929b2		48890c24		MOVQ CX, 0(SP)
  test.go:9		0x10929b6		488b542448		MOVQ 0x48(SP), DX
  test.go:9		0x10929bb		4889542408		MOVQ DX, 0x8(SP)
  test.go:9		0x10929c0		48c744241001000000	MOVQ $0x1, 0x10(SP)
  test.go:9		0x10929c9		e802bef7ff		CALL runtime.mapassign_fast64(SB)
  test.go:9		0x10929ce		488b442418		MOVQ 0x18(SP), AX
  test.go:9		0x10929d3		48c70001000000		MOVQ $0x1, 0(AX)
  test.go:10		0x10929da		488d053f6b0100		LEAQ runtime.rodata+92288(SB), AX
  test.go:10		0x10929e1		48890424		MOVQ AX, 0(SP)
.....

可以看到对于两个变量n、m,虽然是两个不同的变量,但是由于类型相同,都是map[int]int,因此这两个实例使用的runtime类型结构体是一样的。以上代码说明,类型模板结构体存储在runtime.rodata区域,是程序的只读数据区,程序的所有类型模板结构体都将存储在这个位置。

复杂类型实例在runtime的具体实现在slice.go,chan.go,map.go,string.go,iface.go中,而其他基本简单类型的实现由于没有涉及复杂算法,只通过在内存中平铺bit就可以,因此直接由编译器完成。

需要注意的是,这里所说的实现指的是数据类型使用内存的方式,上一节的类型模板没有涉及到这点。

reflect实现

正如代码注释中所说,golang通过reflect实现了类型反射,使得程序可以在运行时检测给定变量的类型

// Package reflect implements run-time reflection, allowing a program to
// manipulate objects with arbitrary types. The typical use is to take a value
// with static type interface{} and extract its dynamic type information by
// calling TypeOf, which returns a Type.

interface{}是reflect的关键,go通过它可以实现面向对象中多态的功能。

reflect的实现主要包含在type.go和value.go中,为了理解reflect实现,需要先了解interface的内部机制,因为任何类型变量要使用reflect提供的功能,首先需要将该变量赋值到interface变量上,reflect只针对interface变量:


 s := "hello world"
 var a interface{}

 a = s
 t := reflect.TypeOf(a)
 v :+ reflect.ValueOf(a)

runtime interface: runtime interface的实现在runtime2.go中:

//empty  interface
type eface struct {
	_type *_type
	data  unsafe.Pointer
}

//interface
type iface struct {
	tab  *itab //第一个字段也是_type
	data unsafe.Pointer
}

这里只关注empty interface。empty interface可以容纳任何类型的变量,从它的实现结构体中也可以看出来,第一个字段标识变量的类型,第二个字段为指针,可以指向任意类型的值。比如对于slice变量,当将它赋值给interface变量时,先将runtime/type.go中的slicetype赋值给_type,然后再将runtime/slice.go中的slice赋值给data字段。一个type,一个value。

runtime中还提供了一系列转换函数以负责将没有显示实现类型模板的简单基本类型转化为interface:

func convT2E(t *_type, elem unsafe.Pointer) (e eface)
func convT2E16(t *_type, val uint16) (e eface)
func convT2Estring(t *_type, val string) (e eface) 
....

这些conv函数中的第一个_type参数由编译器从编译完成后的可执行文件中提取。

reflect: 以上的实现说明了可以把任意类型的变量赋值给empty interface,在此基础上,就可以实现reflect机制。reflect的两个最重要的对外接口函数是:

func TypeOf(i interface{}) Type {
	eface := *(*emptyInterface)(unsafe.Pointer(&i))
	return toType(eface.typ)
}

func ValueOf(i interface{}) Value {
	if i == nil {
		return Value{}
	}

	// TODO: Maybe allow contents of a Value to live on the stack.
	// For now we make the contents always escape to the heap. It
	// makes life easier in a few places (see chanrecv/mapassign
	// comment below).
	escapes(i)

	return unpackEface(i)
}

这里的参数都是interface,因此内结构都是eface。下面分别讲述这两个函数的功能,以及基于它们的返回值可以做些什么事情。

TypeOf TypeOf函数的功能比较简单明了,就是从eface中拿出type字段。它的返回值是一个Type。从结构体字段来说,它在reflect中对应的是:

type rtype struct { //rtype实现了Type接口
	size       uintptr
	ptrdata    uintptr  // number of bytes in the type that can contain pointers
	hash       uint32   // hash of type; avoids computation in hash tables
	tflag      tflag    // extra type information flags
	align      uint8    // alignment of variable with this type
	fieldAlign uint8    // alignment of struct field with this type
	kind       uint8    // enumeration for C
	alg        *typeAlg // algorithm table
	gcdata     *byte    // garbage collection data
	str        nameOff  // string form
	ptrToThis  typeOff  // type for pointer to this type, may be zero
}

这和本文最开始介绍的runtime内部的type公共结构体是一模一样的。因此TypeOf的作用就一目了然了,它就是提取出类型模板,同时reflect通过为type提供丰富的对外接口使得应用程序可以通过调用rtype的实现接口获得更完善的类型信息。所有的这些接口都实现在Type中:

type Type interface{
    ...
}

所有类型的变量通过reflect.TypeOf获得返回值后,都可以通过这些接口获得更详细的类型信息,包括tag,name,method等。

ValueOf 根据TypeOf的实现经验,比较容易会觉得ValueOf函数就是返回eface的data字段,不过事实不是这样。ValueOf返回一个Value结构体,

type Value struct {
	// typ holds the type of the value represented by a Value.
	typ *rtype

	// Pointer-valued data or, if flagIndir is set, pointer to data.
	// Valid when either flagIndir is set or typ.pointers() is true.
	ptr unsafe.Pointer

	// flag holds metadata about the value.
	// The lowest bits are flag bits:
	//	- flagStickyRO: obtained via unexported not embedded field, so read-only
	//	- flagEmbedRO: obtained via unexported embedded field, so read-only
	//	- flagIndir: val holds a pointer to the data
	//	- flagAddr: v.CanAddr is true (implies flagIndir)
	//	- flagMethod: v is a method value.
	// The next five bits give the Kind of the value.
	// This repeats typ.Kind() except for method values.
	// The remaining 23+ bits give a method number for method values.
	// If flag.kind() != Func, code can assume that flagMethod is unset.
	// If ifaceIndir(typ), code can assume that flagIndir is set.
	flag

	// A method value represents a curried method invocation
	// like r.Read for some receiver r. The typ+val+flag bits describe
	// the receiver r, but the flag's Kind bits say Func (methods are
	// functions), and the top bits of the flag give the method number
	// in r's type's method table.
}

Value比eface的data要复杂一些,我们甚至可以认为ValueOf其实什么都没有做,它就是把eface简单封装了一遍,然后就返回了:

// unpackEface converts the empty interface i to a Value.
func unpackEface(i interface{}) Value {
	e := (*emptyInterface)(unsafe.Pointer(&i))
	// NOTE: don't read e.word until we know whether it is really a pointer or not.
	t := e.typ //type可以只是头而已,可以表示任何type
	if t == nil {
		return Value{}
	}
	f := flag(t.Kind())
	if ifaceIndir(t) {
		f |= flagIndir
	}
	return Value{t, e.word, f}
}

这是ValueOf的核心实现,这么看来Value和eface并没有什么本质的不同。当然,由于它包含了eface的data字段,因此它能实现一些Type实现不了的接口,亦即获得一些通过Type获得不到的信息,以cap接口为例:

// Cap returns v's capacity.
// It panics if v's Kind is not Array, Chan, or Slice.
func (v Value) Cap() int {
	k := v.kind()
	switch k {
	case Array:
		return v.typ.Len()
	case Chan:
		return chancap(v.pointer())
	case Slice:
		// Slice is always bigger than a word; assume flagIndir.
		return (*sliceHeader)(v.ptr).Cap
	}
	panic(&ValueError{"reflect.Value.Cap", v.kind()})
}

对于v.Kind() == slice来说,type是绝对不能获得slice的长度的,可以看到这里是通过访问data指针字段得到的。

Value还提供了一系列的Set接口来设置变量的值。

总结

本文简单介绍了golang的类型实现机制,然后基于类型实现机制研究了reflect的实现原理。这里没有涉及各个类型的详细实现细节,只是探讨了实现框架。golang在runtime为复杂的数据类型构造了类型结构体模板,每种类型在runtime阶段都会对应一个类型模板存储在数据区。需要注意类型与实例的区别,类型模板是编译器构造,runtime期间只读的,而每个实例都属于某个类型模板下,但是实例在runtime期间是可变的。reflect通过实现一套和runtime对应的结构体和接口,来实现类型反射机制,interface是reflect的基础,而interface在runtime对应的就是eface(依然只考虑empty interface)。eface可容纳各种类型的特性为reflect的实现提供了基础。

blog comments powered by Disqus