Channel是Golang的核心类型,是goroutine间通讯的一种方式。
通道channel
channel基本使用示例:
make(chan TYPE)
创建通道ch<- v
发送数据v:=<-ch
接收数据close(ch)
关闭通道
var ch chan int
ch = make(chan int)
go func() {
for i := 0; i < 10; i += 1 {
ch <- i + 1
time.Sleep(time.Second)
}
close(ch)
}()
// v := <- ch // 读取一个
for v := range ch { // 读取所有,直到通道关闭
fmt.Println(v)
}
原理
channel是go语言层面提供的协程间通讯方式;其由队列、类型信息、goroutine等待队列等组成;
type hchan struct {
qcount uint // 当前队列中剩余元素个数
dataqsiz uint // 环形队列长度, 即可以存放的元素个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 每个元素的大小
closed uint32 // 标识关闭状态
elemtype *_type // 元素类型
sendx uint // 队列下标, 指示元素写入时存放到队列中的位置
recvx uint // 队列下标, 指示元素从队列的该位置读出
recvq waitq // 等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列
lock mutex // 互斥锁, chan不允许并发读写
}
chan内部通过一个环形队列作为缓冲区,长度在创建时指定。
等待队列:
- 缓冲区为空或没有缓冲区(创建时长度为0),读取goroutine会被阻塞;会被写goroutine唤醒;
- 缓冲区已满或没有缓冲区,写goroutine会被阻塞;会被读数据的goroutine唤醒;
- 一个channel同时仅被允许一个goroutine读写;
创建/关闭
创建channel过程ch := make(Chantype, size int)
实际是初始化hchan结构:容量(size)在创建时确定,以后不能修改(省略为0);size=0时为非缓冲channel。
关闭channel(close(ch)
)时会:
- 把等待的读取goroutine(recvq上)全部唤醒,且数据设为“零值”(指针则为nil);从未初始化的通道(nil)接收元素会导致永久的阻塞。
- 把等待的写goroutine(sendq上)全部唤醒,且产生panic;
channel相关的panic还有:
- 关闭值为nil的channel;
- 关闭已经关闭的channel;
- 向已关闭的channel写数据;
因此不要在接收端关闭通道(在发送端关闭)
select
select可监控多个channel:
- 多个case满足条件时(可执行),执行顺序是随机的;
- 若所有case分支都不能执行,会执行default分支(若有);
- 若没有default,且全部case不可执行时,会阻塞;
- 无论分支是否被选中执行,case关键字右边的表达式都会先求值(从左到右,自上而下);
ches := []chan int{
make(chan int),
make(chan int),
}
defer func() {
for _, ch := range ches {
close(ch)
}
}()
numbers := []int{1, 2}
//go func() { <-ches[0] }()
getChan := func(n int) chan int {
fmt.Println("Get chan: ", n)
return ches[n]
}
getNum := func(n int) int {
fmt.Println("Get num: ", n)
return numbers[n]
}
select {
case getChan(0) <- getNum(0):
fmt.Println("1st case selected")
case getChan(1) <- getNum(1):
fmt.Println("2ed case selected")
default:
fmt.Println("default selected")
}
// Get chan: 0
// Get num: 0
// Get chan: 1
// Get num: 1
// default selected
// 若去掉<-ches[0]的注释(先等待读取数据),则输出为
// Get chan: 0
// Get num: 0
// Get chan: 1
// Get num: 1
// 1st case selected
虽然两个case都没有被选中,但是右边表达式上的函数还是执行了。
range
range可持续从channel中读取数据(像遍历数组一样),无数据时会阻塞:
for v := range ch {
fmt.Println("Value: ", v)
}
非阻塞读写
默认从通道中读取数据时(range或select),当无数据时会阻塞,若要不阻塞则需要使用带default的select:
非阻塞读
select {
case n := <- ch:
fmt.Println("Got:", n)
default:
fmt.Println("no data")
}
非阻塞写
select {
case ch <- n:
fmt.Println("Set:", n)
default:
fmt.Println("channel full")
}
清空
有时清空channel中已有的数据也是需要的:
for len(ch)>0{
<-ch
}
单向channel
单向channe只是对channel的一种使用约束限制(一般用于参数传递):
<-chan int
:只读channel,只能从中读取数据;chan<- int
:只写channel,只能写入数据;
定时器(time包)
time包中一些功能是用通道辅助实现的。
Timer一次性定时器
Timer是一种单一事件的定时器,即经过指定的时间后触发一个事件并结束。
time.NewTimer(d Duration) *Timer
:创建定时器;timer.Stop()
:停止定时器;timer.Reset(d Duration)
:重置定时器(可用于修改还未到期的定时器;先停掉,再启动);
Timer创建后便开始计时,时间到达后向Timer管道中发送当前时间(做为事件)。
简单接口:
After(d Duration) <-chan Time
:创建定时器(为匿名定时器,不能对其操作,只能等待超时),并返回其管道;AfterFunc(d Duration, f func()) *Timer
:到期后,执行指定的函数。
超时示例接口:
ch := make(chan int, 1)
go func() {
time.Sleep(time.Second)
ch <- 1
}()
tmer := time.NewTimer(600 * time.Millisecond)
for i := 0; i < 3; i++ {
select {
case v := <-ch:
fmt.Println("Received: ", v)
case v := <-tmer.C: //<-time.After(600 * time.Millisecond):
fmt.Println("Timeout: ", v)
tmer.Reset(600 * time.Millisecond)
}
}
fmt.Println("Stop: ", tmer.Stop())
// Timeout: 2022-01-13 08:57:36.4402573 +0800 CST m=+0.607663601
// Received: 1
// Timeout: 2022-01-13 08:57:37.0639077 +0800 CST m=+1.231328401
// Stop: true
Ticker周期性定时器
Ticker是周期性定时器,即周期性的触发一个事件(事件触发时,若前面的事件还未被取走,则当前事件直接丢弃)。
time.NewTicker(d Duration) *Ticker
:创建一个定时器;ticker.Stop()
:停止定时器;
Ticker在使用完后务必要释放,否则会产生资源泄露(会持续消耗CPU资源)。
简单接口:
Tick(d Duration) <-chan Time
:创建一个用不会停止的定时器(匿名的),并返回其管道;
ch := make(chan int, 1)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
go func(count int) {
i := 0
for _ = range ticker.C {
select {
case ch <- 0:
case ch <- 1:
}
i++
if i > count {
break
}
}
close(ch)
}(10)
for v := range ch {
fmt.Print(v, " ")
}
fmt.Println()
// 0 1 1 0 0 1 1 1 0 0 0