string
c语言string,本质上char数组,结尾用\0作为分割符。所以string实际元素,不可以包含\0。
go语言string,本质上是struct,结构如下,首元素地址+长度。所以go可以包含\0作为元素。
type stringStruct struct {
str unsafe.Pointer
len int
}
slice
go语言slice,是数组结构的再封装,支持扩容。结构如下
type slice struct {
array unsafe.Pointer
len int
cap int
}
在go语言实际开发中,array作为函数参数值传递,而slice是引用传递。底层数组不变。
且通过数组实例切片的时候,引用的底层数组就是此数组。修改值的时候要当心
slice扩容规则:
1,预估扩容
所需容量大于当前容量的二倍,预估值则为所需容量
所需容量小于当前容量的二倍,且待扩容总数小于1024,直接翻倍
所需容量小于当前容量的二倍,且待扩容总数大于1024,按照1/4扩容
2,内存对其
拿到预估容量,需要考虑内存对齐,需要根据元素大小,和预估容量计算出所占用的
字节数,然后通过字节数向上取最接近的字节数。
通过最接近的字节数,拿到真实的预估容量。
struct
结构体,理解成可以封装不同数据结构的一个总的数据结构,属于自定义数据结构
结构体指针指向首元素地址
结构体要考虑内存对其。根据计算机32位,64位的不同,内存对齐的规则不同。
反正内存对其的核心就是减少cpu通过地址总线加载数据的次数。能一次加载到就不加载多次。
虽然有时候会造成内存的浪费。
map
map结构,底层一般是hash表实现。
常见hash函数
1,取模 hash%m
2,位与运算 hash & (m-1) m必须是2的整数次幂
hash冲突解决方法:
1,拉链法
2,开放地址法
hash表扩容
1,hash负载因子,判断是否需要扩容
count/m
2,渐进式扩容
当判断hash表需要扩容,则分配足够多的空间,且并不是直接copy旧空间的数据到新空间
因为这样可能会带来瞬时性能抖动。
渐进式扩容,就是标记当前hash表为需要扩容状态,并且记录新老地址以及扩容的偏移量。
在后续每次hash表操作时,迁移部分数据到新地址。多次操作过程中,完成扩容。修改
hash表状态位非扩容装填。扩容完成。
go语言map
本质上,go语言map是一个做过高级封装的hash表
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
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
}
溢出桶
// mapextra holds fields that are not present on all maps.
type mapextra struct {
// If both key and elem do not contain pointers and are inline, then we mark bucket
// type as containing no pointers. This avoids scanning such maps.
// However, bmap.overflow is a pointer. In order to keep overflow buckets
// alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.
// overflow and oldoverflow are only used if key and elem do not contain pointers.
// overflow contains overflow buckets for hmap.buckets.
// oldoverflow contains overflow buckets for hmap.oldbuckets.
// The indirection allows to store a pointer to the slice in hiter.
overflow *[]*bmap
oldoverflow *[]*bmap
// nextOverflow holds a pointer to a free overflow bucket.
nextOverflow *bmap
}
注意:扩容是迫不得已才进行扩容的,能不扩容则不扩容。当超过负载因子的时候,先用溢出桶方案进行间接扩容
go语言map扩容规则
1,map的count在预先申请的内存中存满了,则优先使用溢出桶,作为简介扩容方案。
2扩容条件:
当使用的溢出桶过多,则需要进行扩容。当超过负载因子(go为6.5)的时候进行翻倍扩容
负载因子未超标,却有很多溢出桶的情况,进行等量扩容(map删除很多元素的情况下)
函数
一个进程必须有:代码段,堆栈段,数据段
简单调用过程
函数调用过程中,call指令,跳转到被调用函数入口。被调用函数执行完毕,ret指令跳回调用处继续执行
栈帧,分配给函数的栈空间
栈基,函数栈帧的栈底
栈指针,栈顶
go语言函数栈帧布局:
|调用者栈基地址|
|局部变量|
|返回值(调用函数的返回值)|
|参数|
解析:1,被调用函数是通过栈指针+偏移量定位到返回值和参数
2,go语言的栈帧不是逐步变大,而是直接分配好栈帧,栈指针指向开头,然后通过栈指针+偏移量定位指令
call指令:
1,下一条指令的地址入栈
2,跳转到被调用函数入口处执行
ret执行:
1,弹出调用函数下一条的指令地址
2,跳到调用函数的下一条指令地址继续执行调用函数
执行过程:
调用函数,执行call指令,先入栈下一条指令的地址
call函数跳到被调用函数入口
被调用函数开始执行,开辟栈帧(修改bp,sp寄存器的值即可)
被调用函数执行完毕,恢复调用者栈基
执行ret,弹出返回地址
跳到调用函数下一条指令地址,进行执行
注意的问题:
1,go函数中,return语句执行结束,再执行defer函数
2,go函数中,如若调用者指定了返回值,被调用函数不需要再定义,此值并非被调用函数的局部变量
修改此值都会在调用函数生效。
闭包
1闭包函数,有状态的函数
在函数执行过程中,会分配funcValue结构体。指向函数地址。
状态参数固定值,则拷贝即可。状态函数非固定值(有修改情况),则堆上分配状态参数的地址。如果闭包执行多次,
最后状态函数都会给修改。
最终目标:
闭包函数底层设计的最终目标,其实就是保持状态参数,参数,返回值的一致性。