Golang Chan

2020/12/18Friday 编程 Golang

最近有人和我讨论 go 语言存不存在引用类型, 我想我连宇宙的尽头在哪都不知道,怎么会知道 golang 里的引用类型呢?

直到找到这样一本通俗 golang 教程, 看了其中的go101 - 引用, 才解开我对所谓“引用”的疑惑.

字符串

byte = uint8 对应字节

byte 即字节, 在计算机中用于表示编码一个字符所占用的大小, 一般是8位二进制

rune = int32 对应码点

在 golang 中, 所有的字符串常量都被视为是 utf-8 编码的, 在编译时刻非法的 utf-8 编码字符串将导致编译失败

在 utf-8 中, 一个码点值可能由1-4个字节组成, 中文一般由3个字节组成, 转成 rune 操作将保证 utf-8 的中文字符不会乱码

字符串操作

在 golang 中, 字符串虽然是持有指针类型, 但底层却不允许修改, 所以将字符串转换成 []byte 或 []rune 都将深拷贝一份其底层字符串

在字符串过大的情况下深拷贝将会影响效率, 一般来说

作为rune使用:

for _, v := range(str) {
	// 这里是int32 也就是rune类型
	fmt.Printf("%T\n", v)
}

作为byte使用:

for i:=0; i<len(str); i++ {
	// 字符串切片将会作为uint8 也就是byte类型
	fmt.Printf("%T\n", str[i])
}

以上操作都不会拷贝字符串, 但 range 和切片操作不一致确实有点反直觉

通道

通道内部维护三个队列:

  • 接收数据协程队列
  • 发送数据协程队列
  • 缓冲数据队列

每个通道内部还会维护一个互斥锁用来防止数据竞争

协程首先尝试获取通道内部的锁, 拿到锁之后

接收数据 x = <-ch

  1. 如果此通道的缓冲数据队列不为空, 从中弹出一个值作为接收. 同时判断如果发送数据协程队列不为空, 将从中弹出一个发送协程, 将该发送协程的发送值填充缓冲数据队列, 再切换该发送协程为运行态
  2. 如果缓冲数据队列为空,但发送数据协程队列不为空, 直接弹出一个发送协程, 取其值作为接收, 将该发送协程切换至运行状态
  3. 如果缓冲数据队列发送数据协程队列同时为空, 将当前协程推入接收数据协程队列, 此操作为一个阻塞操作

发送数据 ch<- x

  1. 如果此通道的接收数据协程队列不为空(此时缓冲数据队列必定为空), 弹出一个接收数据协程队列的接收协程, 将当前协程发送的值做为该接收协程的接收值, 再切换该接收协程为运行态
  2. 如果缓冲数据队列未满(此时发送数据协程队列必定为空), 将要发送的值推入缓冲数据队列
  3. 接收数据协程队列为空且缓冲数据队列已满, 将当前协程推入发送数据协程队列, 此操作为一个阻塞操作

关闭通道 close(ch)

  1. 如果此通道的接收数据协程队列不为空, 弹出此队列中所有协程并每个协程接收一个该通道类型的零值然后切换至运行态
  2. 如果此通道的发送数据协程队列不为空, 弹出此队列中所有协程并每个协程产生一个panic(向已关闭通道发送数据)

接收数据协程队列发送数据协程队列中存的都是在阻塞中等待被其他协程操作该通道唤醒的协程

关闭一个多发送的通道

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 1)
	// 根据以上规则,下面两个协程必定一个先抢到到 ch 的锁
	// 此时拿到锁的协程判断 ch 的缓冲数据队列未满, 将要发送的数据推入缓冲数据队列, 结束运行
	// 另一个协程开始获得锁, 判断接收数据协程队列为空并且缓冲数据队列已满(1)
	// 将此协程推入发送数据协程队列(该协程切换为阻塞态)
	go func() {
		ch <- 2
	}()
	go func() {
		ch <- 3
	}()
	// 等待上面的协程操作执行完
	time.Sleep(1 * time.Second)
	// 关闭通道, 通道中发送数据协程队列中的所有协程将产生一个painc
	close(ch)
	// 这里等待上面的协程出现painc并将错误抛出, 不然main函数就先跑完了
	time.Sleep(1 * time.Second)
}

如果在上面例子两个协程go func() {}中都加入painc捕获:

defer func() {
	if err := recover(); err != nil {
		fmt.Println("painc: ", err)
	}
}()

函数将正常运行下去:

IMPORTANT

从一个已经关闭的通道中接收数据不会产生painc, 是先取完缓冲数据队列的值, 再返回该通道类型对应的零值

select case

  1. 将所有 case 操作随机排序
  2. 获取所有 case 相关通道锁
  3. 按随机后的顺序检查各个 case
    1. 如果是向一个关闭的通道发送数据, 逆序解锁所有 case 相关通道, 当前协程产生一个panic, break
    2. 如果该通道操作是非阻塞的或者是 default 分支(default也是非阻塞的),逆序解锁所有 case 相关通道, 执行该 case/default 分支的代码块, break
  4. 将当前协程推入到每个 case 操作对应的 接收数据协程队列发送数据协程队列
  5. 当前协程进入阻塞态, 等待根据上面的唤醒队列协程规则被唤醒
  6. 当前协程被以下操作唤醒
    1. 通道关闭操作, 跳转到 3.
    2. 通道数据接收/发送操作, 将当前协程从相应case操作相关的通道的接收/发送数据协程队列中移除, 逆序解锁所有通道并执行 case 分支代码块