值部和赋值

值的分类

Go可以被看作是一门C语言血统的语言,这可以通过此前的指针和结构体两篇文章得以验证。 Go中的指针和结构体类型的内存结构和C语言很类似。

另一方面,Go也可以被看作是C语言的一个扩展框架。 在C中,值的内存结构都是很透明的;但在Go中,对于某些类型的值,其内存结构却不是很透明。 在C中,每个值在内存中只占据一个内存块(一段连续内存);但是,一些Go类型的值可能占据多个内存块

以后,我们称一个 Go 值分布在不同内存块上的部分为此值的各个值部(value part)。 一个分布在多个内存块上的值含有一个直接值部和若干被此直接值部引用着的间接值部

每个值在内存中只分布在一个内存块上的类型每个值在内存中会分布在多个内存块上的类型
单值部多值部
布尔类型
各种数值类型
指针类型
非类型安全指针类型
结构体类型
数组类型
切片类型
映射类型
通道类型
函数类型
接口类型
字符串类型
  • 注意:
    • 接口类型字符串类型值是否包含间接部分取决于具体编译器实现。 如果不使用今后将介绍的非类型安全途径,我们无法从这两类类型的值的外在表现来判定它们的值是否含有间接部分。 在本系列中,我们认为这两类类型的值是可能包含间接值部的。
    • 同样地,函数类型的值是否包含间接部分几乎也是不可能验证的。 在本系列中,我们认为函数类型是可能包含间接值部的。

在本系列中的大多数文章中,如果没有特别说明,当一个指针类型被谈及,它表示一个类型安全指针。 但是在本文的余下内容中,当一个指针被谈及,它可能表示一个类型安全指针,也可能表示一个非类型安全指针。

一个指针值存储着另一个值的地址,除非此指针值是一个 nil 空指针。 我们可以说此指针引用着另外一个值,或者说另外一个值正被此指针所引用。 一个值可能被间接引用,比如:

  • 如果一个结构体值 a 含有一个指针字段 b 并且这个指针字段 b 引用着另外一个值 c,那么我们可以说结构体值 a也引用着值 c。
  • 如果一个值 x(直接或者间接地)引用着另一个值 y,并且值 y(直接或者间接地)引用着第三个值 z,则我们可以说值 x 间接地引用着值 z。

以后,我们将一个含有(直接或者间接指针字段的结构体类型称为一个指针包裹类型,将一个含有(直接或者间接指针的类型称为指针持有者类型。 指针类型和指针包裹类型都属于指针持有者类型。元素类型为指针持有者类型的数组类型也是指针持有者类型

指针持有者类型:指针类型,指针包裹类型,含有指针的类型

指针包裹类型:含有指针字段的结构体

间接值部类型的(可能的)内部实现

切片

type slice struct {
    array unsafe.Pointer // 引用着底层的元素
    len   int            // 当前的元素个数
    cap   int            // 切片的容量
}

一个切片类型在内部可以看作是一个指针包裹类型。 每个非零切片值包含着一个底层间接部分用来存储此切片的元素。 一个切片值的底层元素序列(间接部分)被此切片值的array字段所引用。

字符串

type stringStruct struct {
    str unsafe.Pointer // 引用着底层的byte元素
    len int            // 字符串的长度
}

每个字符串类型在内部也可以看作是一个指针包裹类型。 每个非零字符串值含有一个指针字段 str。 这个指针字段引用着此字符串值的底层字节元素序列。

接口

我们可以认为接口类型在内部是如下定义的:

type _interface struct {
    dynamicType  *_type         // 引用着接口值的动态类型
    dynamicValue unsafe.Pointer // 引用着接口值的动态值
}

接口类型也可以看作是一个指针包裹类型。一个接口类型含有两个指针字段。 每个非零接口值的(两个)间接部分分别存储着此接口值的动态类型和动态值。 这两个间接部分被此接口值的直接字段dynamicTypedynamicValue所引用。

上面这个内部定义只用于表示空接口类型的值。空接口类型没有指定任何方法。 后面的接口一文详细解释了接口类型和值。 非空接口类型的内部定义如下:

type _interface struct {
	dynamicTypeInfo *struct {
		dynamicType *_type       // 引用着接口值的动态类型
		methods     []*_function // 引用着动态类型的对应方法列表
	}
	dynamicValue unsafe.Pointer // 引用着动态值
}

一个非空接口类型的值的dynamicTypeInfo字段的methods字段引用着一个方法列表。 此列表中的每一项为此接口值的动态类型上定义的一个方法,此方法对应着此接口类型所指定的一个的同描述的方法。

映射、通道和函数类型的内部定义

// 映射类型
type hmap struct {
    // ...
    buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
    oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
    nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)
    extra *mapextra // optional fields
}

// 通道类型
type hchan struct {
    buf      unsafe.Pointer // points to an array of dataqsiz elements
    elemtype *_type // element type
  // ...
}

// 函数类型
type _function *functionImpl

同样,这些类型在实现上都包含一个或者多个指针。

为什么结构体类型不是间接值部类型

为什么结构体类型不是间接值部类型?结构体的字段是切片或者map这些间接值部类型呢?

在赋值中,底层间接值部将不会被复制

现在我们了解了第二个分类中的类型的内部结构是一个指针持有(指针或者指针包裹)类型。 这对于我们理解Go中的值复制行为有很大帮助。

在 Go 中,每个赋值操作(包括函数调用传参等)都是一个值的浅复制过程(假设源值和目标值的类型相同)。 换句话说,在一个赋值操作中,只有源值的直接部分被复制给了目标值。 如果源值含有间接部分,则在此赋值操作完成之后,目标值和源值的直接部分将引用着相同的间接部分。 换句话说,两个值将共享底层的间接值部,如下图所示:

赋值

事实上,对于字符串值和接口值的赋值,上述描述在理论上并非百分百正确官方FAQ明确说明了在一个接口值的赋值中,接口的底层动态值将被复制到目标值。 但是,因为一个接口值的动态值是只读的,所以在接口值的赋值中,官方标准编译器并没有复制底层的动态值。这可以被视为是一个编译器优化。 对于字符串值的赋值,道理是一样的。所以对于官方标准编译器来说,上一段的描述是100%正确的

TODO: 举例说明

因为一个间接值部可能并不专属于任何一个值,所以在使用unsafe.Sizeof函数计算一个值的尺寸的时候,此值的间接部分所占内存空间未被计算在内

关于术语“引用类型”和“引用值”

“引用”这个术语在Go社区中使用得有些混乱。很多Go程序员在Go编程中可能由此产生了一些困惑。 一些文档或者网络文章,包括一些官方文档,把“引用”(reference)看作是“值”(value)的一个对立面。 本系列强烈不推荐这种定义。这里仅仅列出一些肯定错误地使用了“引用”这个术语的例子:

  • 在Go中,只有切片、映射、通道和函数类型属于引用类型。 (如果我们确实需要引用类型这个术语,那么我们不应把其它指针持有者类型排除在引用类型之外。)
  • 一些函数调用的参数是通过引用来传递的。 (在Go中,所有的函数调用的参数都是通过值复制直接值部的方式来传递的。)

我并不是想说引用类型这个术语在Go中是完全没有价值的, 我只是想表达这个术语是完全没有必要的,并且它常常在Go的使用中导致一些困惑。我推荐使用指针持有者类型来代替这个术语。 另外,我个人的观点是最好将引用这个词限定到只表示值之间的关系,把它当作一个动词或者名词来使用,永远不要把它当作一个形容词来使用。 这样将在使用Go的过程中避免很多困惑。