Go语言常见数据结构的实现详解

 更新时间:2023年07月27日 11:33:31   作者:叶枫桦  
这篇文章主要为大家学习介绍了Go语言中的常见数据结构(channal、slice和map)的实现,文中的示例代码简洁易懂,需要的可以参考一下

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数据结构内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Go|使用Options模式和建造者模式创建对象实战

    Go|使用Options模式和建造者模式创建对象实战

    这篇文章主要介绍了Go使用Options模式和建造者模式创建对象实战详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04
  • 基于golang的轻量级工作流框架Fastflow

    基于golang的轻量级工作流框架Fastflow

    这篇文章主要介绍了基于golang的轻量级工作流框架Fastflow,fastflow 执行任务的过程会涉及到几个概念:Dag, Task, Action, DagInstance,本文给大家分享完整流程,需要的朋友可以参考下
    2022-05-05
  • Golang实现WebSocket服务的项目实践

    Golang实现WebSocket服务的项目实践

    本文介绍如何使用Golang实现实时后端WebSocket服务,首先使用Gin框架搭建http服务,然后使用gorilla/websocket库实现简单后端WebSocket服务,具有一定的参考价值,感兴趣的可以了解一下
    2023-05-05
  • Go语言基础类型及常量用法示例详解

    Go语言基础类型及常量用法示例详解

    这篇文章主要为大家介绍了Go语言基础类型及常量的用法及示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助
    2021-11-11
  • 一文详解go同步协程的必备工具WaitGroup

    一文详解go同步协程的必备工具WaitGroup

    这篇文章主要为大家介绍了一文详解go同步协程的必备工具WaitGroup使用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03
  • Go计算某段代码运行所耗时间简单实例

    Go计算某段代码运行所耗时间简单实例

    这篇文章主要给大家介绍了关于Go计算某段代码运行所耗时间的相关资料,主要介绍了Golang记录计算函数执行耗时、运行时间的一个简单方法,文中给出了详细的代码示例,需要的朋友可以参考下
    2023-11-11
  • Golang协程常见面试题小结

    Golang协程常见面试题小结

    本文主要介绍了Golang协程常见面试题小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-02-02
  • Golang解析JSON遇到的坑及解决方法

    Golang解析JSON遇到的坑及解决方法

    这篇文章主要为大家介绍了Golang解析JSON时会遇到的一些坑及解决方法,文中的示例代码讲解详细,对我们学习Go语言有一点的帮助,需要的可以参考一下
    2023-02-02
  • Golang内存泄漏场景以及解决方案详析

    Golang内存泄漏场景以及解决方案详析

    golang中内存泄露的发现与排查一直是来是go开发者头疼的一件事,下面这篇文章主要给大家介绍了关于Golang内存泄漏场景以及解决的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-01-01
  • 浅谈Go用于同步和并发控制的几种常见锁

    浅谈Go用于同步和并发控制的几种常见锁

    本文主要介绍了浅谈Go用于同步和并发控制的几种常见锁,包括互斥锁、读写锁和一次性锁等,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-08-08

最新评论