likes
comments
collection
share

go入门之路(一)常见数据结构和实现

作者站长头像
站长
· 阅读数 60

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分别表示队列中的索引

写入的流程图

go入门之路(一)常见数据结构和实现 读的流程图

go入门之路(一)常见数据结构和实现

常用语法

单向管道

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然后输出执行完毕 go入门之路(一)常见数据结构和实现

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]++
   }
}

先看看这段代码输出结果是

go入门之路(一)常见数据结构和实现 这是为什么呢? 因为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)
}

go入门之路(一)常见数据结构和实现 我们看到这里并没有改变数组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数组上进行拉链法追加

go入门之路(一)常见数据结构和实现

可以在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去实现

struct

go语言中struct类似于别的语言中的class,在go中定义私有和共有变量是通过第一个字母大小写去决定的,大写表示公有小写表示私有

这里说一下go语言中的继承,由于go中没有直接的继承或者接口实现的关键字都是通过隐形和组合的方式去实现的

组合

比如实现一个人的类和一个雇员类可以使用以下方式

type Person struct {
    Name string
    Age  int
}

type Employee struct {
    Person  // 实现组合
    EmployeeID int
}

这样就可以达到继承的效果可以在Employee类中去访问Name和Age字段

接口

go中的接口定义之后,只要你实现了接口中的定义就表示实现了接口,例如以下代码实现一个shape接口只要有area方法就表示实现了这个接口在传递参数的时候就可以使用这个接口去传递而不是直接使用实现类去传递

type Shape interface {
    Area() float64
}

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func PrintArea(s Shape) {
    fmt.Println("Area:", s.Area())
}

func main() {
    rect := Rectangle{
        Width:  10,
        Height: 5,
    }

    PrintArea(rect) // Rectangle 实现了 Shape 接口
}

字段标签

go中最特殊的就是可以设置tag标签有点类似于注解,这个需要使用go为我们提供的reflect包去反射获得到这个标签,先使用Typeof获取它反射出来的类型然后通过它为我们提供的方法即可获取到tag

type person struct {
   name string `json:"name"`
   age  int    `json:"age" bson:"age"`
}

func main() {
   t := person{"user", 18}
   of := reflect.TypeOf(t)
   for i := 0; i < of.NumField(); i++ {
      fmt.Println(of.Field(i).Name, of.Field(i).Tag)
   }
}

结构体中每一个字段都表示一个field,它在reflect包中结构体的定义如下

type StructField struct {
   // Name is the field name.
   Name string
   // PkgPath is the package path that qualifies a lower case (unexported)
   PkgPath string
   Type      Type      // field type
   Tag       StructTag // field tag string
   Offset    uintptr   // offset within struct, in bytes
   Index     []int     // index sequence for Type.FieldByIndex
   Anonymous bool      // is an embedded field
}

其实也就是包括了name、type、tag这些属性,然后如何获取到的其实就是字符串解析,然后放入对应的结构体中

同时我们可以根据structtag去自己实现functag就像是swagger在go中的使用那样虽然go中现在还没装饰器,但是我们可以通过文件读取通过字符串访问拼接对注释进行解析然后生成对应的代码随后添加到tag中这样就可以实现functag功能

同时go中结构体是可以直接进行比较的这个和数组类似

type person struct {
   name string
}

func main() {
   p1, p2 := person{"David"}, person{"David"}
   fmt.Println(p1 == p2, &p1 == &p2)
}

输出结果如下

go入门之路(一)常见数据结构和实现 可以看出这两个是不同的对象但是通过==比较后的结果是相同的

string

go中string是一个基础类型,它里面是存的是utf8格式去存储的,所以要注意一下存储汉字的时候,由于一个汉字由多个字节去组成因此遍历的时候可能不连续

func main() {
   s := "中国"
   fmt.Println(len(s))
   for i := 0; i < len(s); i++ {
      fmt.Print(s[i], " ")
   }
   fmt.Println()
   for _, i := range s {
      fmt.Print(i, " ", ch, " ")
   }
}

go入门之路(一)常见数据结构和实现

我们由此看见len(s)的长度是6同时使用range去遍历的时候i也不是连续的

在go中string拼接的时候都会重新进行一次内存分配,多次拼接只会进行一次内存分配,这可能出现性能的浪费,可以使用strings.Buffer或者是fmt.Strintf这两种方法去拼接字符串提高性能


func main() {
	// 创建一个新的 strings.Builder
	var sb strings.Builder

	// 向字符串缓冲区追加内容
	sb.WriteString("Hello, ")
	sb.WriteString("World!")

	// 获取拼接后的字符串
	result := sb.String()

	// 打印结果
	fmt.Println(result)
}

go中通常会让string和btys[]进行转换转换起来也是十分简单的,但是它也是会从新开辟空间使用的时候得注意

[]btye(str) // string转btyes
 
string(b)   // btys转string
转载自:https://juejin.cn/post/7260107455035834427
评论
请登录