Golang常用数据结构实现原理

Chan数据结构

src/runtime/chan.go:hchan定义了channel的数据结构:

type hchan struct {
    qcount   uint           // 当前队列中剩余元素个数
    dataqsiz uint           // 环形队列长度,即可以存放的元素个数
    buf      unsafe.Pointer // 环形队列指针
    elemsize uint16         // 每个元素的大小
    closed   uint32            // 标识关闭状态
    elemtype *_type         // 元素类型
    sendx    uint           // 队列下标,指示元素写入时存放到队列中的位置
    recvx    uint           // 队列下标,指示元素从队列的该位置读出
    recvq    waitq          // 等待读消息的goroutine队列
    sendq    waitq          // 等待写消息的goroutine队列
    lock mutex              // 互斥锁,chan不允许并发读写
}

chan示意图
dataqsiz指示了队列长度为6,即可缓存6个元素;
buf指向队列的内存,队列中还剩余两个元素;
qcount表示队列中还有两个元素;
sendx指示后续写入的数据存储的位置,取值[0, 6);
recvx指示从该位置读取数据, 取值[0, 6);

recvq waitq // 等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列

从channel读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞。
向channel写数据,如果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞。

被阻塞的goroutine将会挂在channel的等待队列中:

  • 因读阻塞的goroutine会被向channel写入数据的goroutine唤醒;
  • 因写阻塞的goroutine会被从channel读数据的goroutine唤醒;

elemtype *_type // 元素类型

一个channel只能传递一种类型的值,类型信息存储在hchan数据结构中。

lock mutex // 互斥锁,chan不允许并发读写

一个channel同时仅允许被一个goroutine读写

Slice数据结构

源码包中src/runtime/slice.go:slice定义了Slice的数据结构:

type slice struct {
    array unsafe.Pointer //指针
    len   int //长度
    cap   int //容量
}

看一下slice的特性


func main() {
	//1.slice
	var array [10]int
	array[5] = 100
	array[6] = 101
	var slice = array[5:6]//传递的是指针
	slice[0] = 200
	fmt.Println("lenth of slice: ", len(slice))
	fmt.Println("capacity of slice: ", cap(slice))
	fmt.Println(&slice[0] == &array[5])
	fmt.Println("array: ", array)
	
	//2.slice
	var slice []int
	slice = append(slice, 1, 2, 3, 4)
	fmt.Println("cap of slice :", cap(slice))
	newSlice := append(slice, 5)
	fmt.Println(&slice[0] == &newSlice[0])
	//================================
	var slice []int
	slice = append(slice, 1, 2, 3, 4, 5)
    fmt.Println("cap of slice :", cap(slice))
	newSlice := append(slice, 6)
	fmt.Println(&slice[0] == &newSlice[0])
}

第一个输出的是
lenth of slice: 1
capacity of slice: 5
true //证明slice使用的是array 的地址
array: [0 0 0 0 0 200 101 0 0 0]
第二个输出的是
cap of slice : 4
false
========
cap of slice : 6
true

解释:append函数执行时会判断切片容量是否能够存放新增元素,如果不能,则会重新申请存储空间,新存储空间将是原来的2倍或1.25倍(取决于扩展原空间大小),本例中实际执行了两次append操作,第一次空间增长到4,所以第二次append需要扩容,所以新旧两个切片所用存储空间不一样。程序会输出”false”。
第二个案例,第一次空间增加到6,第二次append时不需要扩容,所以为true

Slice 扩容机制

使用append向Slice追加元素时,如果Slice空间不足,将会触发Slice扩容,扩容实际上是重新分配一块更大的内存,将原Slice数据拷贝进新Slice,然后返回新Slice,扩容后再将数据追加进去。

Slice Copy

使用copy()内置函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的切片指向的数组中,拷贝数量取两个切片长度的最小值。
也就是说,copy过程中不会发生扩容。

下面是一个面试常见的算法题,其中就用到copy

package main

import "fmt"

// 题目:给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
// 示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
var (
	path []int
	res  [][]int
)

func main() {
	arr := combine(4, 2)
	fmt.Println(arr)
}

func combine(n int, k int) [][]int {
	path, res = make([]int, 0, k), make([][]int, 0)
	dfs1(n, k, 1)
	return res
}
func dfs1(n, k int, start int) {
	if len(path) == k {
		temp := make([]int, k)
		copy(temp, path)     //这里需要先进行copy,再添加,否则所有添加的结果统一
		res = append(res, temp)
	}

	for i := start; i <= n; i++ {
		path = append(path, i)
		dfs1(n, k, i+1)
		path = path[:len(path)-1]
	}

}

总结:

  • 每个切片都指向一个底层数组
  • 每个切片都保存了当前切片的长度、底层数组可用容量
  • 使用len()计算切片长度时间复杂度为O(1),不需要遍历切片
  • 使用cap()计算切片容量时间复杂度为O(1),不需要遍历切片
  • 通过函数传递切片时,不会拷贝整个切片,因为切片本身只是个结构体而已
  • 使用append()向切片追加元素时有可能触发扩容,扩容后将会生成新的切片

map

map数据结构由runtime/map.go:hmap定义:

type hmap struct {
    count     int // 当前保存的元素个数
    ...
    B         uint8
    ...
    buckets    unsafe.Pointer // bucket数组指针,数组的大小为2^B
    ...
}

hmap.B=2为例, 此时hmap.buckets长度是2^B为4. 元素经过哈希运算后会落到某个bucket中进行存储,也就是经过hash运算之后数据会落在4个桶之中的一个

bucket数据结构

bucket数据结构由runtime/map.go:bmap定义:


type bmap struct {
    tophash [8]uint8 //存储哈希值的高8位
    data    byte[1]  //key value数据:key/key/key/.../value/value/value...
    overflow *bmap   //溢出bucket的地址
}

每个bucket可以存储8个键值对。

  • tophash是个长度为8的数组,哈希值相同的键(准确的说是哈希值低位相同的键)存入当前bucket时会将哈希值的高位存储在该数组中,以方便后续匹配。
  • data区存放的是key-value数据,存放顺序是key/key/key/…value/value/value,如此存放是为了节省字节对齐带来的空间浪费.
  • overflow 指针指向的是下一个bucket,据此将所有冲突的键连接起来。

下图展示bucket存放8个key-value对

在这里插入图片描述
当有两个或以上数量的键被哈希到了同一个bucket时,我们称这些键发生了冲突。Go使用链地址法来解决键冲突。

下图展示产生冲突后的map:
在这里插入图片描述

map查询过程

查找过程如下:

  1. 根据key值算出哈希值
  2. 取哈希值低位与hmap.B取模确定bucket位置
  3. 取哈希值高位在tophash数组中查询
  4. 如果tophash[i]中存储值也哈希值相等,则去找到该bucket中的key值进行比较
  5. 当前bucket没有找到,则继续从下个overflow的bucket中查找。
  6. 如果当前处于搬迁过程,则优先从oldbuckets查找

注:如果查找不到,也不会返回空值,而是返回相应类型的0值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值