Go语言常见数据结构的实现详解
作者:叶枫桦
channal
channal是go中的管道,主要用于协程之间的通信,他有点类似于阻塞队列,使用管道可以简单的实现生产者消费者,他会帮助我们自动的去阻塞或者唤醒groutine
创建写入和写出
c := make(chan int, 5) c <- 1 v := <-c
channal中如果是nil的话读取和写入都不会触发panic并且阻塞groutine如果是关闭的channal的话是可以读取的但是不能写如果写的话就会触发panic
channal的源码在runtime/chan.go中下面是结构体
type hchan struct { qcount uint // total data in the queue dataqsiz uint // size of the circular queue buf unsafe.Pointer // points to an array of dataqsiz elements elemsize uint16 closed uint32 elemtype *_type // element type sendx uint // send index recvx uint // receive index recvq waitq // list of recv waiters sendq waitq // list of send waiters }
根据结构体我们也不难发现它的数据结构其实就是一个循环队列,同时又有两个recvq和sendq去代表写操作和读操作的阻塞队列,qcount表示当前使用大小也就是len(),dataqsiz表示容积大小也就是cap(),buf表示真实存储的地址recvx和sendx分别表示队列中的索引
写入的流程图
读的流程图
常用语法
单向管道
func test1(c chan<- int) {} // 只读 func test2(c <-chan int) {} // 只写
可以传递chan的读或者写这样在方法中只能进行一种操作
select多路监听
使用select可以监听多个channel的读或者写,select如果不写default的话,会阻塞groutine有可以读取到的才会唤醒,写了default的话如果都不满足条件就会执行default中的代码不会阻塞
func main() { c1 := make(chan string) c2 := make(chan string) go func() { time.Sleep(1 * time.Second) c1 <- "one" }() go func() { time.Sleep(1 * time.Second) c2 <- "two" }() for i := 0; i < 2; i++ { select { case msg1 := <-c1: fmt.Println(msg1) case msg2 := <-c2: fmt.Println(msg2) } } fmt.Println("执行完毕") }
上述代码的结果是运行之后1秒输出one和two然后输出执行完毕
for-range
可以使用for-range的方式去channel中不断的读取数据它会在没有数据的时候阻塞线程
func main() { c1 := make(chan string) go func() { for { time.Sleep(1 * time.Second) c1 <- "one" } }() chanRange(c1) fmt.Println("执行完毕") } func chanRange(c chan string) { for e := range c { fmt.Println(e) } }
上述代码中使用了一个for-range在主线程去读取数据会阻塞同时开启一个groutine去每秒钟写入一个one上述代码的结果就是每秒输出one并且"执行完毕"永远也不会执行
slice
切片是我们平时最常用的,它又称为动态数组,它的底层是类似于java中的arrayList的会根据当前容量自动扩容,这样如果不理解一下它的原理有的问题是不好发现的
func main() { s1 := []int{1, 2} s2 := s1 s2 = append(s2, 3) sliceRise(s1) sliceRise(s2) fmt.Println(s1, s2) } func sliceRise(s []int) { s = append(s, 0) for i := range s { s[i]++ } }
先看看这段代码输出结果是
这是为什么呢? 因为slice底层是使用一块内存地址的,只有当容量不够的时候才会创建新的地址,然后将之前的值复制上去
因为s1是array它的空间就是2,s2=s1这样s2和s1指向一块地址,s2 = append(s2, 3)这个语句由于s2中的空间不够因此需要扩容2倍就导致s1和s2不是一块地址了而是两块不同的,进行增加操作的时候s1内存不足新创建一块导致增加的不是原本的数,s2空间是4因此可以再装一个数因此操作的时候还是操作原来的数,这就导致s1中的数没增加,s2中的数增加了
slice的源码在runtime/slice.go中
type slice struct { array unsafe.Pointer len int cap int }
它的结构体也是非常简单,就是一个数组和长度容积大小
切片在使用的时候就是a[low:hight]这种格式表示前闭后开
a = a[:len(a) - 1] // 表示删除最后一个 a = a[1:] //表示删除第一个 a = [1,2,3,4] fmt.Println(a[1:3]) // 输出结果为2,3
由于底层使用的是同一块地址因此会出现下面的问题
func main() { a := []int{1, 2, 3, 4, 5} b := a[1:3] b = append(b, 0) fmt.Println(a) }
我们看到这里并没有改变数组a只是操作切片b就导致a中的数据发生改变,因为这里的b,len大小为2但是cap的大小为4就导致在原来的地址上面修改了
因此提供了一种设置大小的方式就是第三个参数
b := a[1:3:3]
b的声明改成这样就可以让cap的大小为2保证数据安全
数组的直接比较
同时这里也聊一聊go中数组的语法糖:我们可以直接使用==去比较两个数组
a := [2]int{1,2} b := [2]int{1,2} fmt.Printf(a == b) // true
如果数组中长度和里面的数都是相等的话可以使用==去比较两个数组是否相等
map
map是我们最常用的数据结构之一,如果学习过别的语言例如java就对map的数据结构比较熟悉,比如扰动函数、hashcode、负载因子、哈希冲突等名词都十分熟悉
在go中map的实现是通过bucket这种方式实现的,其实就是一个数组,计算需要存入的值然后找到数组下标,找到bucket中每一个下标代码的是一个8长度的数组同一个hashcode可以存8个值,如果出现哈希冲突就在这8数组上进行拉链法追加
可以在runtime/hashmap中去查找grow的代码
// grow the map func (hmap *hmap) grow() { // ... // compute new size newBucketsCount := oldBucketsCount if !hmap.growing() { newBucketsCount = oldBucketsCount << 1 } // ... newBuckets := makeBucketArray(newBucketsCount) // ... for i := 0; i < oldBucketsCount; i++ { // ... for e := oldBuckets[i].first; e != nil; e = e.next { // ... // rehash the key to find the new bucket bucket := hashKey(newBuckets, e.key) // ... // insert the element into the new bucket newBuckets[bucket].insert(e) } } // ... }
扩容过程就是创建一个长度二倍的bucket然后对旧的每一个数进行重新hash然后放入新的bucket中
在go中map是线程不安全的如果想要线程安全可以使用sync包中的map去实现
到此这篇关于Go语言常见数据结构的实现详解的文章就介绍到这了,更多相关Go数据结构内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!