1. 结构体和指针
2. 字符串:
-
用一个2字长的数据结构表示。它包含一个指向字符串存储数据的指针和一个长度数据 切分
操作 str[i:j] 会得到一个新的2字长结构,一个可能不同的但仍指向同一个字节序列(即上文说的存储数据)的指针和长度数据。这意味着字符串切分可以在不涉及内存分配或 复制操作。这使得字符串切分的效率等同于传递下标
3、slice
- 是一个数组某个部分的引用
- 含3个域的结构体:指向slice中第一个元素的指针,slice的长 度,以及slice的容量。长度是下标操作的上界,如x[i]中i必须小于长度。容量是分割操作的上界,如x[i:j]中j不能大于容量
- 数组的slice并不会实际复制一份数据,它只是创建一个新的数据结构,包含了另外的一个指针,一个长度和一个容量数据。 如同分割一个字符串,分割数组也不涉及复制操作:它只是新建了一个结构来放置一个不同的指针,长度和容量
- slice的扩容:C语言动态数组的实现在$GOROOT/src/pkg/runtime/runtime.h 可以看到实现
4、make和new
new(T) 返 回一个 *T ,返回的这个指针可以被隐式地消除引用(图中的黑色箭头)。
make(T, args) 返回一个普通的T。通常情况 下,T内部有一些隐式的指针(图中的灰色箭头)。
一句话,new返回一个指向已清零内存的指针,而make返回一个复杂的 结构
5、slice与unsafe.Pointer相互转换
6、map
在底层是用哈希表实现的,你可以在 $GOROOT/src/pkg/runtime/hashmap.goc 找到它的实现。
hmap
结构体Hmap中包含B(容量),bucketsize ,bucket 和oldbucket几个元素
其中BUCKETSIZE是用宏定义的8,每个bucket中存放最多8个key/value对, 如果多于8个,那么会申请一个新的bucket,并 将它与之前的bucket链起来
这个hash结构使用的是一个可扩展哈希的算法,由hash值mod当前hash表大小决定某一个值属于哪个桶,而hash表大小是2 的指数,即上面结构体中的2^B。每次扩容,会增大到上次大小的两倍。结构体中有一个buckets和一个oldbuckets是用来实 现增量扩容的。正常情况下直接使用buckets,而oldbuckets为空。如果当前哈希表正在扩容中,则oldbuckets不为空,并且 buckets大小是oldbuckets大小的两倍
设计细节:Bucket中key/value的放置顺序,是将keys放在一起,values放在一起,如果是这样的一个map[int64]int8,考虑到字节对齐,会 浪费很多存储空间。
增量扩容
如果扩容前的哈希表大小为2B,扩容之后的大小为2(B+1),每次扩容都变为原来大小的两倍,哈希表大小始终为2的指数 倍,则有(hash mod 2^B)等价于(hash & (2^B-1))。这样可以简化运算,避免了取余操作。
假设扩容之前容量为X,扩容之后容量为Y,对于某个哈希值hash,一般情况下(hash mod X)不等于(hash mod Y),所以扩容 之后要重新计算每一项在哈希表中的新位置。当hash表扩容之后,需要将那些旧的pair重新哈希到新的table上(源代码中称之 为evacuate), 这个工作并没有在扩容之后一次性完成,而是逐步的完成(在insert和remove时每次搬移1-2个pair),Go语 言使用的是增量扩容。
优点:主要是缩短map容器的响应时间。
增量扩容本质上还是将总的扩容时间分摊到了每一次哈希操作上面
扩容会建立一个大小是原来2倍的新的表,将旧的bucket搬到新的表中之后,并不会将旧的bucket从oldbucket中删除,而是 加上一个已删除的标记。
查找过程
- 根据key计算出hash值。
- 如果存在old table, 首先在old table中查找,如果找到的bucket已经evacuated,转到步骤3。 反之,返回其对应的 value。
- 在new table中查找对应的value。
这里一个细节需要注意一下。不认真看可能会以为低位用于定位bucket在数组的index,那么高位就是用于key/valule在 bucket内部的offset。事实上高8位不是用作offset的,而是用于加快key的比较的。
插入过程分析
- 根据key算出hash值,进而得出对应的bucket。 2. 如果bucket在old table中,将其重新散列到new table中。 3. 在bucket中,查找空闲的位置,如果已经存在需要插入的key,更新其对应的value。 4. 根据table中元素的个数,判断是否grow table。 5. 如果对应的bucket已经full,重新申请新的bucket作为overbucket。 6. 将key/value pair插入到bucket中。
2.4 nil的语义
1.interface
未初始化 interface ,是nil,空指针
2.string和slice
string的空值是"",它是不能跟nil比较的。即使是空的string,它的大小也是两个机器字长的
slice也类似,它的空值并不是 一个空指针,而是结构体中的指针域为空,空的slice的大小也是三个机器字长的。
3.channel和map
它们在栈上只是一个指针,实际的数据都是由指针所指向的堆上面。未初始化为nil
channel读写规则
读或者写一个nil的channel的操作会永远阻塞。
读一个关闭的channel会立刻返回一个channel元素类型的零值。
写一个关闭的channel会导致panic。
3 函数调用协议
1.Go调用汇编和C
只要不使用C的标准库函数,Go中是可以直接调用C和汇编语言的。其实道理很简单,Go的运行时库就是用C和汇编实现 的,Go必须是能够调用到它们的。当然,会有一些额外的约束,这就是函数调用协议。 Go中调用汇编
2 多值返回
3 go关键字
4 defer关键字
defer和go一样都是Go语言提供的关键字。defer用于资源的释放,会在函数返回之前进行调用。
如果有多个defer表达式,调用顺序类似于栈,越后面的defer表达式越先被调用。
函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中