• GO语言: 双单链表、队列、进出栈打造一个简易的数据结构库 以及测试你的程序是否存在BUG!


    GO语言进阶知识学习

    通过双单链表、队列、进出栈打造一个简易的数据结构库
    以及测试你的程序是否存在BUG
    战斗环境:Ubuntu20.4TS

    GO语言介绍:

    Go(又称Golang)是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。罗伯特·格瑞史莫(Robert Griesemer),
    罗勃·派克(Rob Pike)及肯·汤普逊(Ken Thompson) Go的语法接近C语言,但对于变量的声明有所不同。Go支持垃圾回收功能。
    Go的并行模型是以东尼·霍尔通信顺序进程(CSP)为基础,采取类似模型的其他语言包括OccamLimbo,但它也具有Pi运算的特征,
    比如通道传输。在1.8版本中开放插件(Plugin)的支持,这意味着现在能从Go中动态加载部分函数。

    个人体验感受以及如何入坑:

    前面介绍的肯·汤普逊(Ken Thompson) 是谁?如果不是计算机专业或行业内可能没听过,但是计算机教科书一定会出现一个名词“ 贝尔实验室”,
    没错就是那个传奇计算机研究中心中最高的殿堂。像大名鼎鼎的C语言和UNIX操作系统,目前的计算机系统亦或是安卓苹果系统都是基于,
    这个编程语言语言和操作系统。作为开发者之一的肯·汤普逊,也是GO语言开发之一。如果接触过GO语言的玩家可以看到整个结构很接近C语言,
    但又有很大不同,如果感兴趣可以自行学习。
    如何入坑的?
    当时自己在学习c语言和python进度都非常慢,一直提高不了效率和开发不错的项目,大概限制在于C语言的开发难度,和python开发一些项目的速度不快的原因。
    导致很难看到尽头,为了改变这种困境所以想做一些项目,为了应对现在时代变迁需求,选择往web方向。经过一顿百度操作,有了初略的开发学习思路,
    一些PHP世界第一, javascript, java, css html 不断从我眼前闪烁。当时连什么前端后端都分不清楚,也不知道框架是什么,看了很久文章也不知道从哪里下手。
    看到了PHP的口碑,就买了几本PHP书籍可以学习,从一开始就各种碰壁,不知道是我太笨还是这个作者教学太欠抽,根本看不懂,那只能跟着敲了吧?
    一般都是php直接对接HTML那时候还没接触到前后端分离React.js Vue.js这些词汇。虽然跟着敲很快一个粗糙的西式网站主页就出来了,但是依旧不懂原理代码也很快忘记。
    过程就为了找书中提到的加速插件,就让我发疯网上早已停止供应,崩溃。因为工作原因PHP学习就这样以失败告终。
    后面如何接触GO?
     由于自身空闲的时间比较多了,想着继续开发WEB,但是PHP感觉完全没学到。开始在网上找文章如何搭建WEB偶然 的机会在GO语言社区了解到GO语言,
    看到社区搭建的风格都很活力而且速度很快。所以想着了解一下发现:如下图
    由于GO语言诞生比较新,所以社区比较活跃,加上有世界科技最前沿的公司Google在维护。
    在网上买了几本GO语言的书籍,开始重新学习。与C语言的学习模式很像。
     

    打嘴炮时间结束


    第一种:通过git下载源码,根据自己习惯去放置位置:
    git clone https://github.com/EternalNight996/go-tools.git
    cd go-tools
    go run index.go

     第二种:用我们go语言自带的,跟上面获取的方式一样。但是这种不用产生新文件夹,会管理起来

    go get github.com/EternalNight996/go-tools/container
    cd $GOPATH/src
    git clone https://github.com/EternalNight996/go-tools.git
    cd go-tools.git
    gvim index.go

    那这些包都存储到哪里了? 要么$GOPATH/src/........ 要么$GOROOT/...
    但是我下载到的是$GOPATH/pkg/mod, go get 的好处,是GO可以自动去$GOPATH/pkg 和 src里去调用包,不需要我们手动管理,推荐!!!但我们要下载示范码...
    输出的结构分别对应的的是各个接口,这里用的是gvim编辑器,根据自己喜好。

     

    先有概要调用,以及我们可以看到调用的是github.com/EternalNight996/go-tools/container,这里大概关于golang不同package之间的互通原理及如何控制,
    照顾下新人。因为我自己也被坑过!!!!我们玩过GO都知道开头 package main  是主要输出的包,类似c语言中的 main() {}把主要输出运行放在这个函数里面。
    但事实上我们这个包的名字正确是叫什么?go-tools为什么呢?我打开github下载的go.mod 这个是什么?事实上是项目管理,还有另一个当我们建立了go.mod后
    go get something? 以及运行自动go get的时候,go 就会自动给我们创建一个文件名为go.sum的,是主要放置go get 的所有版本记录和包。 go.mod也会记录,
    但是只记录主要的包,和目前我们这个项目的包名和go version。  

     

    所以我们导入包的时候,前面要加github....,事实上项目只是go-tools/+子项目名如container。
    输入vim container/double_list.go或者vim container/double_list_test.go我们可以看到下图

     

     这里又一个坑,我已经踩过一次了,这里的package不是随便取的,一定要对应文件夹名。通过文件夹统一内容共享。

     

    双链表与单链表


    先进入container打开单链表
    cd container
    gvim single_list.go

    下面即是所有内容的重点,如果这里的逻辑关系没搞清楚,双链表以及后面的队列 和出入栈。都会受到影响

     

    上面是我们单链表代码,下面是我们一开始示范接口的开始段。我们先建立了一个new()去连接container.SingleList,
    从上面可以看到类型是 type SingleList struct,是类型结构。SingleList结构非常简单。
    如果你是刚学GO或者学了GO还没深入了解* 这个字符的意思,这里我小小科普。接触*是我主要在学
    C语言时接触到的,即指针。不同场合不同作用如 a *= 1那可能是a = a * 1, 但是放在赋值的下面举例:
    var source int = 10
    var i int = source 
    var j *int = &source
    i = i + 4
    *j = *j - 4
    fmt.Println("source==", source) //source== 6发现source我们没去操作值被剪掉了4!
    fmt.Println("i==", i) //i== 14 既然没被减?
    fmt.Println("j==", j) //j== 0xc00008c010 这是什么?是分配source所在的地址,&source。
    fmt.Println("*j==", *j) //*j== 6 同理我们操作source -= 1或 *j -= 1两个值都守捆绑,实现同步
    j就是那个被赋予指针指向&source的那个指针变量,这样可能看的懂一点了,但是还在神游这就是指针的恐怖之处,
    这只是指针的冰山一角,尤其是C语言的指针复杂程度超乎其他编程语言哈哈。。逃离C语言的指针支配是正确的选择。我们只需要知道我们需要的知识即可。
    mutex *sync.RWMutex(Read Write Mutex)读写互斥锁用途就是防止goroutine抢资源读写出错,后面有细讲。
    HEAD *SingleNode  我们根据上面我们可以知道*可以让我们HEAD与 SingleNode结构实现同步,试想一下。
    如果我们HEAD一直与一个数组[0]同步,那么每次我们获取HEAD的时候相当是获取[0]无论值是什么样。
    TAIL *SingleNode  如果一个数组[0, 1, 2, 3, 4, 5, 6.........N]一直扩张,而我们又不能在不同的函数或不同的位置去修改变量,
    除了工作量大了代码多了,还容易出错。指针就很重要,因为他是同步的。他不接收实质的值,
    他只接收内存地址去取。只要每次插入或者更新列表时候,将列表的最后一个值与TAIL相连接就可以实现尾部同步了。
    Length uint  唯一的一个常量,uint为32位长度到底是什么这里不细讲,你就当他是一个没有负整数的整数。
    这个可以方便我们去快速锁定位置,和知道目前SingleFace最大的长度。
    type SingleFace  interface {} 节点数据 这个是什么?这就是GO语言的强大之处,他相当于一个高级的指针接口,
    什么是高级指针?就是不需要我们去管理指针,我们也知道在C语言中指针多了,就会出现野指针等,特别容易系统崩溃。
    这是什么情况,&container这格式是不是有点眼熟?如果我们把&con....打印出来,
    大概的格式如*container.DoubleNode 那么我们用指针呢?0xc00000c1e没错事实上interface{}
    要干的事情就是将这些0x00000c1e十六进制地址存储并标记起来是数据结构还是整数字符串,
    抑或是函数也能存储。所以说强大对吧?   ype SingleNode struct { DATA SingleFace; NEXT *SingleNode },
    如果我们用过归递和代送,都知道它们能循环去做某项任务直到任务完成或者错误。有点老鸟就说了“不就是,
    底层0 | 1原理吗?”基础很好,很多虽然知道,但就是不能很好的解释和弄懂。
    有点新鸟可能连归递与代送区别都没弄懂,这里还是照顾新人。下面代码以代送与归递实现相同功能。
    //代送打印100内的偶数
    for i := 1; i < 101; i++ {
      if i%2 == 0 {
        fmt.Println(i)
      }
    }
    //归递打印100内的偶数
    func funcTest(one int) {
        if one < 101 {
            if one%2 == 0 {
                fmt.Println(one)
            }
            funcTest(one + 1)
        }
    }
    funcTest(1)
    虽然同样能实现一样的结果,但由于底层机制不相同,归递除了效率没代送高而且容易走火。
    链表节点也是一样的,如果我们加判断语句那么就会容易在内存的海洋迷失!
    心细的玩家就会发现,DATA DoubleFace怎么没赋予指针?假设我们赋予指针那么会发生什么?
    我们不断添加数据,而不是一次性。那么上面例子的DoubleFace可提取的内容是什么?
    "SingleAppend-3"只要我们去掉了指针,就可以实际的存储值,每次赋值的时候,内存就会分配一个新内存空间!
    最后解释SingleNode,  如果我们需要加入新的存储内容就需要通过SingleNode,通过SingleNode指向SingleFace。
    有的说为什么不把SingleList放到SingleNode?如果我们将SingleList的TAIL和HEAD..放到SingleNode里行不行?
    当然是可以,但是我们分层就可以方便管理了。当这里核心的内容就讲完了。面我们就看下如何构建一个完整的存储系统?
     

    初始化Init()


     

    现在知道为什么要分SingleList和SingleNode吧?是为了实现频繁对象访问,降低资源消耗!
    如果没怎么接触过高级语言的小伙伴,如纯玩C语言的可能对对象有些陌生。也体验不到对象的强大。
    如果我们用平时写函数的方式写这个Init()会如何? 
    func Init(list *SingleList) {
        ....
        ....
    }
    //如何调用呢?
    single_list := new(container.SingleList)
    container.Init(single_list)
    //发现了吧?哈哈乖乖的用高级语言吧
    为什么要进行初始化?我们要知道前面写的结构只是struct声明,是没有赋值表明的状态的。
     

    Append添加数据


     

    func (list *SingleList) Append(node *SingleNode) bool {}
    相信来看我文章的基础都不错,但是也有部分基础比较差,或者不熟悉GO语言,还是花些时间好好解释下。
    上面已经解释过(list *SingleList)Append 是这个对象建立,我们能在函数内通过list指针变量去访问 SingleList,
    这个list是指针变量,也就是说这个使用list发生变化时,那么SingleList也会变化。
    还有点就是我们也可以通过SingleList去访问Append,single_list.Append()这是对象建立的双向性。
    Append(node *SingleNode) bool {}   ()内表明需要我们导入值进去,类型也限制了为*SingleNode,
    也就需要我们给予一个SingleNode结构指针,就是我们只需要导入&SingleNode即可,
    在我们平时调用的时候建立一个函数即可,因为每次输&con....就有些重复了,如果我们建立变量,
    那么如果我们要插入100多个数据呢?也不行对吧?我们可以外部建立个接口就可以解决这些问题。
    bool则是数据类型中的布尔值 0 | 1 或 true | false, 就是会返回bool类型的值,以便我们去判断这个函数是否添加数据成功!
    if node == nil {return false} //如果我们导入的值为空,则返回一个1 | false
    list.mutex.Lock() //加锁,说明我们正在写入
    defer list.mutex.Unlock() //解锁,前面加个 defer事实上就是把解锁,放在函数解锁时。
    if list.Length == 0 { //当我们处于没存储任何值的状态
        list.HEAD = node //那么头就指向 node数据
        list.TAIL = node //那么尾就指向 node数据, 因为只有一个数据。
        list.Length = 1 //长度+1
        return true //返回true | 0
    }
    //前面已经排除空值,未存储值。也就是说后面都是大于1长度的方法
    list.TAIL.NEXT = node //每次都通过尾部的空间的下一层去获取这个值这样就实现扩展了。
    list.TAIL = node //如果不更新TAIL 那么就不会再扩张,因为停留在原处。

    Insert插入值


     区别Append在尾部等多输入一个  uint整型的标识,

    if node == nil || index > list.Length {return false} //不为空,插入位置小于总长度
    //因为append是添加的时候NEXT一定为空的,所以我们只要在TAIL.NEXT存储即可。但是这个插入0的时候可能后面会有值。
    if index == 0 {    
       //未插入时【HEAD NEXT1 NEXT2 NEXT3 TAIL】
       //插入0时node添加HEAD后【node HEAD..】因为HEAD.NEXT是NEXT1.NEXT是NEXT2。。。。。以此类推
       node.NEXT = list.HEAD
       list.HEAD = node //最后只要调整HEAD的指针即可
       list.Length += 1 //这时候总量发生变化+1
    }
    //当非插入0的时候
    ptr := list.HEAD //先获取HEAD部位置
    for i = 1; i < index; i++ {
        ptr = ptr.NEXT //我们先将指针指向要插入的位置
    }
    // 是不是很熟悉 相当 假设我们让a与b交换,但是a = b还是b = a 都部行,那么聪明的你就想到。
    // c = a; a = b; b = c; 那么a 与 b 就成功对换了,这些基础只要学过任何语言都知道。
    next := ptr.NEXT //next等同c 
    ptr.NEXT = node 
    node.NEXT = next

    Get获取


     

    // 在我们获取HEAD,从HEAD开始出发,直到到达index,则将node指针返回
    node := list.HEAD
    var i uint
    for i = 0; i < index; i++ {
       node = node.NEXT
    }
    return node

    Show()打印


    //跟查询一样,但是我们需要走完全程,每走一步就打印当时的数据。
    ptr := list.HEAD
    var i uint
    for i = 0; i < list.Length; i++ {
      fmt.Printf("FLAG:%3d 数据为%vx0A", i+1, ptr.DATA)
      ptr = ptr.NEXT
    }

     

    DoubleList双链表


     

    核心都在单链表讲过了,双链表就概要讲下,如果想了解细节则自己看源码。

    // 双链表节点
    type DoubleNode struct {
    //多了一个PREV 即上层的意思,单表中我们只能够向下走,每次通过HEAD寻找回家路
    //如果有了PREV就可以向上走,大大的提高效率。
    //但是这也意味着,你需要在每个数据中要存储上层指针
        PREV *DoubleNode 
    }
    //如下面Append添加
    else {
        node.PREV = list.TAIL //因为要返回到上层,而Append上层永远是list.TAIL
    }

    //多了一个函数ShowReverse() 就是逆向打印12345变成 54321
    //因为我们单链表没有上层,所以无法通过TAIL用PREV返回上层,所以每次只能通过NEXT
    ptr := list.TAIL
    for ptr != nil {
        ptr = ptr.PREV //跟ptr = ptr.NEXT是一样的
    }
    如果你认真跟我学到这里,我相信你已经吸收了这门知识,并深入了解到了GO语言。结束了吗?还没
     

    queue队列与stack栈


    相信很多人都听说过队列和消息队列,但是却不知道用来干嘛的,什么原理。如果我告诉你们原理却不告诉你们用途,这不是让你们白学了吗?消息队列有什么用途?
    场景说明:用户注册后,需要发注册邮件和注册短信。传统的做法有两种 1.串行的方式;2.并行方式
    串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端

      并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间

    按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,
    直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,
    系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍。看实例吧!!!

    //队列与出入栈都是用SingleList做示例
    //与SingleList建立连接
    // Queue 队列信息
    type Queue struct {
        list *SingleList
    }
    // Enqueue 进入队列
    func (q *Queue) Enqueue(data interface{}) bool {
        return q.list.Append(&SingleNode{DATA: data})
    }
    // Dequeue 出列
    func (q *Queue) Dequeue() interface{} {
        node := q.list.Get(0)
        if node == nil {
            return nil
        }
        q.list.Delete(0)
        return node.DATA
    }
    事实上就是调用SingleNode,连添加都一样就是改个名字哈哈, 我们下面简单用SingleList写一个.
    single_list := new(container.SingleList)
    single_list.Init()
    single_list.Append(&container.SingleNode{DATA: "data1"}) //把数据1进队列
    single_list.Append(&container.SingleNode{DATA: "data2"}) //把数据2进队列
    single_list.Append(&container.SingleNode{DATA: "data3"}) //把数据3进队列
    //我们现在完成出队列了
    fmt.Println(single_list.Get(0).DATA)
    single_list.Delete(0)
    //这两部就是出队列的核心,因为每次只提取头部数据,发现了吧?
    //出栈是提取尾部,出队列是提取头部。
    single_list.Get(single_list.Length)
    single_list.Delete(single_list.Length)
    //这部分就是出栈,其他都差不多,只是函数名定义不同
    其他我就不多讲了,也不需要了。因为最核心的东西你们已经掌握,相信其他挑战就会变得容易。
     

    代码测试


    到了最后一步,这里完了就真结束了。有点说为什么需要代码测试?我们能跑起来不就好了吗?下面看几段测试的代码!!!
    func TestDoubleList_Init(t *testing.T) {
        list := new(DoubleList)
        list.Init()
        if list.Length == 0 {
            t.Log("double list init success")
        } else {
            t.Error("double list init success")
        }
    }

     

     那么测试错误呢?我们讲值改成1

     

    现在我们知道,简单的测试使用和结果。有的人说不就是测试逻辑判断吗?我们加到主代码就可以了,不需要这些额外的,
    显得麻烦。这样你就错了,如果我们没有这些外部的测试程序,我们在日常测试任务就会复杂且庞大许多,
    如我们的项目有几百万行代码分好些个文件夹,各种多。别说几百万行,几千行你可能都需要反复修改代码运行测试。
    这样反而是损耗你的时间,打磨你的精力,还耗损你的设备。我们可以看到container文件夹下有一些_test.go结尾的。
    这些可不是随便命名,这种命名方式GO识别后不会将其导入到我们操作中。它可以测试时嵌入到container,平时不会被调用。
    如何写测试程序且如何测试?我们需要导入一个testing库,testing下有很多功能下面只演示testing.T

     就是正需要测试的部位添加判断返回t.Log()  或 t.Error()就是这么简单,写好测试程序。就可以在外部调用,单条测试.

    go test -run=TestDoubleList_Init

    加-v则有打印内容

    go test -v -run=TestDoubleList_Init

     测试整个项目所有代码,这里不加-v因为内容比较多

    go test

     从入坑到入坟,选择权在你.


     
    我是玫瑰与屠夫,一个逆风前行的玩家。
    2021/5/27
     
     
     
  • 相关阅读:
    JavaScript基础——第四章,JavaScript对象及初识面向对象
    JavaScript基础——第三章,JavaScript操作DOM对象
    Spring框架概述
    swagger注释@API详细说明
    JavaScript基础——第二章,JavaScript操作BOM对象
    JavaScript基础——第一章,基础
    JAVA基础——第二章,变量,数据类型和运算符
    JAVA基础——第一章,初识JAVA
    setinterval取消,element使用正则,git上传,js倒计时
    js复习
  • 原文地址:https://www.cnblogs.com/eternalnight/p/14815804.html
Copyright © 2020-2023  润新知