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

相关文章

  • 详解golang defer 闭包 匿名函数

    详解golang defer 闭包 匿名函数

    这篇文章主要介绍了golang defer 闭包 匿名函数的相关知识,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-07-07
  • Go语言HTTP请求流式写入body的示例代码

    Go语言HTTP请求流式写入body的示例代码

    这篇文章主要介绍了Go语言HTTP请求流式写入body,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-06-06
  • 详解Golang函数式选项(Functional Options)模式

    详解Golang函数式选项(Functional Options)模式

    什么是函数式选项模式,为什么要这么写,这个编程模式解决了什么问题呢?其实就是为了解决动态灵活的配置不同的参数的问题。下面通过本文给大家介绍Golang函数式选项(Functional Options)模式的问题,感兴趣的朋友一起看看吧
    2021-12-12
  • 学习GO编程必备知识汇总

    学习GO编程必备知识汇总

    这篇文章主要介绍了学习GO编程必备知识汇总的相关资料,需要的朋友可以参考下
    2016-07-07
  • Go 结构体、数组、字典和 json 字符串的相互转换方法

    Go 结构体、数组、字典和 json 字符串的相互转换方法

    今天小编就为大家分享一篇Go 结构体、数组、字典和 json 字符串的相互转换方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-08-08
  • Go语言题解LeetCode1260二维网格迁移示例详解

    Go语言题解LeetCode1260二维网格迁移示例详解

    这篇文章主要为大家介绍了Go语言题解LeetCode1260二维网格迁移示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • golang validator参数校验的实现

    golang validator参数校验的实现

    这篇文章主要介绍了golang validator参数校验的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-10-10
  • Go保证并发安全底层实现详解

    Go保证并发安全底层实现详解

    这篇文章主要为大家介绍了Go保证并发安全底层实现详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • Go语言中循环语句使用的示例详解

    Go语言中循环语句使用的示例详解

    在不少实际问题中有许多具有规律性的重复操作,因此在程序中就需要重复执行某些语句。本文将通过示例详细为大家讲讲Go语言中的循环语句,需要的可以参考一下
    2022-04-04
  • golang架构设计开闭原则手写实现

    golang架构设计开闭原则手写实现

    这篇文章主要为大家介绍了golang架构设计开闭原则手写实例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07

最新评论