什么是通道 channel?
”不要通过共享内存来通信 ,而应该通过通信来共享内存“ : 这句话什么意思呢?
举个例子🌰: 如果要把数据传递个另外一个协程, 可以先将数据封装成对象, 然后把对象的指针放进channel 中, 那么目标协程可以从channel中拿到对象指针, 并处理其指向的内存对象。
类似于通信的管道, 虽然有传统共享数据同步机制,但go语言强烈建议我们使用 Channel 通道来实现goroutine 之间的通信
通道就是goroutine 之间的通道, 它可以让goroutine之间相互通信。
通道必须传递自己类型的数据
刚定义的通道 默认是nil的, 可以通过 make(chan int) 来创建
上图 证明通道传的是 内存地址
怎么使用 通道:读操作 + 写操作
data := <- a 从通道 a中取出值, 放到 data中
a <- data ; 将数据data放到 a 这个通道中
package main
import "fmt"
func main(){
var ch1 chan bool
ch1 = make(chan bool)
go func(){
for i:=0; i<4; i++{
fmt.Printf("子goroutine 中 i-> %d\n",i)
}
// 循环结束后 ,向通道中写入数据
ch1 <- true// 该操作可以解除读操作的阻塞
fmt.Println("子goroutine 结束...")
}()
data := <- ch1//先阻塞, 等子gorooutine写入数据后, 该读操作随机解除阻塞
fmt.Println("main goroutine read data :",data)
fmt.Println("main over...")
}
通道 channel 的注意点:
1 用于协程 ,传递消息
2 每个通道都有自己的数据类型 , nil通道,不能直接存储(即先定义,再创建)
3 使用通道传输数据:<-
chan <- data 发送数据到通道,向通道中写数据,
data<-chan 从通道中获取数据
4 阻塞: (必须 有读也有写, 阻塞是同时相互解除的)
发送数据: chan<-data写阻塞的,直到另一个协程读取数据才会解除阻塞,
读取数据: data:= <-chan 也是阻塞的, 直到另一端写出数据解除阻塞
5 本身 通道是同步的, 意味着 同一时间,只能有一条通道来操作。
6 通道是 协程之间的连接, 所以通道的发送和接收必须在不同的协程中。
死锁 : 没有读通道操作, 或者没有写通道操作 ,就会形成死锁
如何 关闭 通道:在子协程中关闭通道
package main
import (
"fmt"
"time"
)
func main(){
/*
关闭通道
*/
ch2 := make(chan int)
go func(){
for i:=0; i<10; i++ {
ch2 <- i
}
fmt.Println("子goroutine 结束...")
close(ch2)
}()
for {
time.Sleep(2 * time.Millisecond)// 保证 数据被写入通道
data2, ok := <-ch2
if ok != true {
fmt.Println("读取通道数据结束...")
break
}
fmt.Println("main goroutine read data ->", data2)
}
fmt.Println("main goroutine over...")
}
对比发现 ,协程直接 也可能因为时间片原因进行抢占调度执行
缓冲通道
不带缓冲的通道: 只有读/写 会发生阻塞
带缓冲的通道: 数据先发送到缓冲区: 只有缓冲区满了 或者空了才会发生阻塞。
创建缓冲通道 只需要在后边 加上通道大小就可以了
通道都是按照队列的数据结构读取的
定向通道:单向通道
ch1 := make(<- chan int) 只能读不能写的通道
ch2 :=make(chan <- int) 只能写 , 不能读的通道
ch3 : =make(chan int) 双向 不带缓冲通道
ch4 : =make(chan int, 4) 双向 带缓冲通道
你可能会问: 既然 只是单向通道:那么又有什么暖用?
是的: 但向通道往往只是作为参数传递的 ,目的就是为了保护而已。
因为通道本来就是用于两个协程之间的通信的, 设置成单向的,往往用处不大
time包下与 通道相关的函数
主要就是 timer定时器, 可以让用户自己定义超时逻辑, 尤其是在 select 处理多个channel 的超时、单channel读写的超时等情况时尤为方便。
Timer是一次性的时间触发事件。Ticker 是按一定的时间间隔 持续触发事件的。
Timer 的触发方式:
一 : t := timer.NewTimer(d)
t := timer.AfterFunc(d,F)
c:=timer.After(d)
可以看到 C是一个只写通道,
package main
import (
"fmt"
"time"
)
func main(){
/*
1 func NewTimer(d Duration)创建一个计时器, d时间以后触发
*/
timer := time.NewTimer(3 * time.Second) //timer是一个time.Timer指针
fmt.Printf("timer type is %T\n",timer)
fmt.Println(time.Now())
//阻塞三秒钟 触发 计时器
ch2 := timer.C //C 是一个通道
fmt.Println(<-ch2)// 读取 ch2内的数据 : 读取的是3秒之后的时间
}
flag :=timer.strop() // 停止定时器
After(d Duration)(chan) // 作用和NewTimer一样 ,但他返回一个通道, 存储当前时间 + d 的时间。
package main
import (
"fmt"
"time"
)
func main(){
/*
1 func NewTimer(d Duration)创建一个计时器, d时间以后触发
*/
timer := time.NewTimer(3 * time.Second) //timer是一个time.Timer指针
fmt.Printf("timer type is %T\n",timer)
fmt.Println(time.Now())
//阻塞三秒钟 触发 计时器
ch2 := timer.C //C 是一个通道
fmt.Println(<-ch2)// 读取 ch2内的数据 : 读取的是3秒之后的时间
fmt.Println()
ch4 := time.After(3 * time.Second)//After 作用和Newtimer一样,但返回一个 通道
fmt.Println(time.Now())
fmt.Println(<-ch4)
}
Select 语句
select 是Go语言提供的一个控制结构(选择语句), 通过select 可以监听在channel上的数据流动。
select和switch 语句非常像, 但是 select 会随机执行一个可运行的case, 如果没有case 可运行的话,有 default 会执行default, 没有的话, select将阻塞, 直到有case 可运行
package main
import "fmt"
func main(){
/*
select和 switch 语句非常相似
有多个case 可运行会随机运行一个,
没有可以运行的 case 会阻塞等待,直到有case 可执行
*/
ch1 := make(chan int)
ch2 := make(chan int)
go func(){
ch1 <-10
}()
//下面我们进行 从通道ch1 ch2中读
select {
case num1 := <-ch1 :
fmt.Println("ch1 中获取数据->",num1)
case num2 ,ok := <-ch2:
if ok{
fmt.Println("ch2 中读取的数据",num2)
}else{
fmt.Println("ch2 通道已经关闭...")
}
}
fmt.Println("main over ...")
}
这个时候 select 就进行随机选择case执行了
package main
import (
"fmt"
"time"
)
func main(){
/*
select和 switch 语句非常相似
有多个case 可运行会随机运行一个,
没有可以运行的 case 会阻塞等待,直到有case 可执行
*/
ch1 := make(chan int)
ch2 := make(chan int)
go func(){
time.Sleep(1* time.Second)
ch1 <-10
}()
go func(){
time.Sleep(1 * time.Second)
ch2 <-20
}()
//下面我们进行 从通道ch1 ch2中读
select {
case num1 := <-ch1 :
fmt.Println("ch1 中获取数据->",num1)
case num2 ,ok := <-ch2:
if ok{
fmt.Println("ch2 中读取的数据",num2)
}else{
fmt.Println("ch2 通道已经关闭...")
}
}
fmt.Println("main over ...")
}
经过测试; select会优先执行 已就绪的语句