channal
channal是go中的管道,主要用于协程之间的通信,他有点类似于阻塞队列,使用管道可以简单的实现生产者消费者,他会帮助我们自动的去阻塞或者唤醒groutine
创建写入和写出
go
c := make(chan int, 5)
c <- 1
v := <-c
channal中如果是nil的话读取和写入都不会触发panic并且阻塞groutine如果是关闭的channal的话是可以读取的但是不能写如果写的话就会触发panic
channal的源码在runtime/chan.go中下面是结构体
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
func test1(c chan<- int) {} // 只读
func test2(c <-chan int) {} // 只写
可以传递chan的读或者写这样在方法中只能进行一种操作
select多路监听
使用select可以监听多个channel的读或者写,select如果不写default的话,会阻塞groutine有可以读取到的才会唤醒,写了default的话如果都不满足条件就会执行default中的代码不会阻塞
go
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中不断的读取数据它会在没有数据的时候阻塞线程
go
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的会根据当前容量自动扩容,这样如果不理解一下它的原理有的问题是不好发现的
go
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中
go
type slice struct {
array unsafe.Pointer
len int
cap int
}
它的结构体也是非常简单,就是一个数组和长度容积大小
切片在使用的时候就是a[low:hight]这种格式表示前闭后开
go
a = a[:len(a) - 1] // 表示删除最后一个
a = a[1:] //表示删除第一个
a = [1,2,3,4]
fmt.Println(a[1:3]) // 输出结果为2,3
由于底层使用的是同一块地址因此会出现下面的问题
css
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就导致在原来的地址上面修改了
因此提供了一种设置大小的方式就是第三个参数
go
b := a[1:3:3]
b的声明改成这样就可以让cap的大小为2保证数据安全
数组的直接比较
同时这里也聊一聊go中数组的语法糖:我们可以直接使用==去比较两个数组
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的代码
go
// 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去实现