golang 复合数据类型

90 阅读9分钟

数组

var a [1]int
var b [3]int
fmt.Println(unsafe.Sizeof(a)) //8
fmt.Println(unsafe.Sizeof(b)) //24

在数组字面值中,如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始化值的个数来计算

q := [...]int{1, 2, 3}

数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

r := [...]int{99: -1}

定义了一个含有100个元素的数组r,最后一个元素被初始化为-1,其它元素都是用0初始化。

如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的

多维数组

var chunks [1 << 1]*[1 << 2]uint64 
var chunks2 [1 << 1][1 << 2]uint64 

log.Println(chunks)//[<nil> <nil>]
log.Println(chunks2)//[[0 0 0 0] [0 0 0 0]]

slice

type slice struct {
   array unsafe.Pointer
   len   int
   cap   int
}
a := []int{}
fmt.Println(unsafe.Sizeof(a))//24

和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较:

但是为何slice不直接支持比较运算符呢?这方面有两个原因。

第一个原因,一个slice的元素是间接引用的,一个slice甚至可以包含自身(译注:当slice声明为[]interface{}时,slice的元素可以是自身)。虽然有很多办法处理这种情形,但是没有一个是简单有效的。

第二个原因,因为slice的元素是间接引用的,一个固定的slice值(译注:指slice本身的值,不是元素的值)在不同的时刻可能包含不同的元素,因为底层数组的元素可能会被修改。

slice唯一合法的比较操作是和nil比较

一个零值的slice等于nil (unsafe.Sizeof 仍然是24)。一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都是0,但是也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。与任意类型的nil值一样,我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。

内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。

make([]T, len)
make([]T, len, cap)

go slice是怎么扩容的

if targetCap > oldCap2, 直接扩容成 targetCap 否则,if oldCap < 256, 直接扩容成 oldCap2 否则,

for 0 < newcap && newcap < cap {
   // Transition from growing 2x for small slices
   // to growing 1.25x for large slices. This formula
   // gives a smooth-ish transition between the two.
   newcap += (newcap + 3*threshold) / 4
}

在循环中扩容成原来的1.25倍,再加上0.75倍threshold(256)

最后再 Round the size up to one of the small size classes

// The main allocator works in runs of pages.
// Small allocation sizes (up to and including 32 kB) are
// rounded to one of about 70 size classes, each of which
// has its own free set of objects of exactly that size.
// Any free page of memory can be split into a set of objects
// of one size class, which are then managed using a free bitmap.

append

每次调用append函数,必须先检测slice底层数组是否有足够的容量来保存新添加的元素。如果有足够空间的话,直接扩展slice(依然在原有的底层数组之上),将新添加的y元素复制到新扩展的空间,并返回slice。因此,输入的x和输出的z共享相同的底层数组。

如果没有足够的增长空间的话,appendInt函数则会先分配一个足够大的slice用于保存新的结果,先将输入的x复制到新的空间,然后添加y元素。结果z和输入的x引用的将是不同的底层数组。每一次容量的变化都会导致重新分配内存和copy操作

map

a := map[string]string{}
fmt.Println(unsafe.Sizeof(a)) //8

When you write the statement

m := make(map[int]int)

The compiler replaces it with a call to [runtime.makemap]

func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap

so map is a pointer to a [runtime.hmap] structure

禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。

Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。

和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较

struct

内存对齐

内存对齐是为了cpu更高效访问内存中数据 && 保证读取数据的原子性

unsafe.Alignof

It returns the largest value m such that the address of v is always zero mod m

如果类型 t 的对齐保证是 n,那么类型 t 运行时的地址必须是 n 的倍数。

For a variable x of any type: unsafe.Alignof(x) is at least 1.
For a variable x of struct type: unsafe.Alignof(x) is the largest of all the values unsafe.Alignof(x.f) for each field f of x, but at least 1.
For a variable x of array type: unsafe.Alignof(x) is the same as the alignment of a variable of the array's element type.

image.png

a := "aa"
fmt.Println(unsafe.Sizeof(a), unsafe.Alignof(a)) // 16,8
a := []string{"aa"}
fmt.Println(unsafe.Sizeof(a), unsafe.Alignof(a)) // 24,8
a := false
fmt.Println(unsafe.Sizeof(a), unsafe.Alignof(a)) // 1,1
a := [1]int8{1}
b := []int8{1}
fmt.Println(unsafe.Alignof(a), unsafe.Alignof(b)) // 1,8
type Some1 struct {
   a bool   //补齐到8byte
   b string //8+16=24
   c bool   // struct的alignment是8,所以这里也要补齐到8byte
}
type Some2 struct {
   a bool
   c bool   //补齐到8byte
   b string //struct size: 8+16=24
}

func TestBasic(t *testing.T) {
   a := Some1{}
   b := Some2{}
   fmt.Println(unsafe.Sizeof(a), unsafe.Sizeof(b)) // 32,24
}

零大小字段对齐

零大小字段(zero sized field)是指struct{}, 大小为0,按理作为字段时不需要对齐,但当在作为结构体最后一个字段(final field)时需要对齐的。 为什么?

因为,如果有指针指向这个final zero field, 返回的地址将在结构体之外(即指向了别的内存),如果此指针一直存活不释放对应的内存,就会有内存泄露的问题(该内存不因结构体释放而释放)所以,Go就对这种final zero field也做了填充,使对齐。

type T1 struct {
   a struct{}
   x int64
}

type T2 struct {
   x int64
   a struct{}
}

func TestBasic(t *testing.T) {
   a := T1{}
   b := T2{}
   fmt.Println(unsafe.Sizeof(a), unsafe.Sizeof(b)) // 8,16
}
  • struct内字段如果填充过多,可以尝试重排,使字段排列更紧密,减少内存浪费
  • 零大小字段要避免作为struct最后一个字段,会有内存浪费
  • 32位系统上对64位字的原子访问要保证其是8bytes对齐的;当然如果不必要的话,还是用加锁(mutex)的方式更清晰简单(这个不用管)

结构体

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。

可以对成员取地址

结构体成员的输入顺序也有重要的意义。我们也可以将Position成员合并(因为也是字符串类型),或者是交换Name和Address出现的先后顺序,那样的话就是定义了不同的结构体类型。

如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决定的。

  • 导出字段:字段名以大写字母开头,包外部可以访问,
  • 非导出字段:字段名以小写字母开头,只能在定义它们的包内部访问
  • 可导出的方法不能返回非导出的struct, 这是个规范,但是在包外部也能调用

一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适用于数组。)但是S类型的结构体可以包含*S指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。

如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0

考虑效率的话,较大的结构体通常会用指针的方式传入和返回

如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。

如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的

只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。

得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:

不幸的是,结构体字面值并没有简短表示匿名成员的语法 结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法,它们彼此是等价的:

gopl.io/ch4/embed

w = Wheel{Circle{Point{8, 8}, 5}, 20}

w = Wheel{
    Circle: Circle{
        Point:  Point{X: 8, Y: 8},
        Radius: 5,
    },
    Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}

匿名成员并不要求是结构体类型;其实任何命名的类型都可以作为结构体的匿名成员。但是为什么要嵌入一个没有任何子成员类型的匿名成员类型呢?

答案是匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。实际上,外层的结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型导出的全部的方法。

json

默认使用Go语言结构体的成员名字作为JSON的对象(通过reflect反射技术,我们将在12.6节讨论)。只有导出的结构体成员才会被编码

结构体的成员Tag可以是任意的字符串面值,但是通常是一系列用空格分隔的key:"value"键值对序列