channel
通道 可以被认为是 Goroutine通信管道
Go 建议使用通道来进行多进程(≥2)之间的通信
定义方式
// var 通道名 chan 数据类型(往通道放什么数据类型)
// 通道名 = make(chan 数据类型)
var ch chan int
a = make(chan int)
使用规则, 存/取
package main
import (
"fmt"
)
// 定义通道 chan
func main() {
// 可以先定义 然后赋值 或者用短定义方式
ch := make(chan int)
// 一个Goroutine 往通道chan放数据
go func() {
for i := 0; i < 10; i++ {
fmt.Println("goroutine-", i)
}
//time.Sleep(time.Second)
ch <- 1
}()
// 另一个Goroutine可以从通道中取出数据(通道之间通信)
// 会阻塞等待ch, 等另一个Goroutine往里面放好数据之后, 再取
data := <-ch
fmt.Println(data)
}
在一个通道里发送和接收数据, 默认是阻塞的。当一个数据被发送到通道时候, 在发送语句中被 阻塞 ( 通道只有一个容量), 直至另一个Goroutine 从该通道中读取数据
channel 是同步的, 意味着同一时间, 只能有一个goroutine来操作。
通道是在不同Goroutine之间的连接, 所有的对通道放和取数据的操作应在不同的Goroutine中(多线程) (不然没有意义)
死锁
产生死锁的方式有很多, 这里主要讲的是由chan 导致的。
即如果创建了chan, 没有Goroutine来使用(没有其他人消费掉数据~), 就会出现死锁
死锁的其他情况:
- 单线程的使用, 没有其他的goroutine消费;或者是先取后放 (初始通道里没有数据嘛)
- 两个chan, 他们互相需要对方的数据, 但由于判断, 拿不到对方的数据
- sync 锁产生的死锁
package main
import (
"fmt"
"time"
)
// 告诉接收方, 不会再有其他数据放到chan里
func main() {
ch1 := make(chan int)
go test1(ch1)
// 读取chan中的数据
for {
time.Sleep(time.Second)
data, ok := <-ch1 // ok 是用于判断chan是否关闭, 若关闭, 则不会再取值
if !ok {
fmt.Println("读取完毕")
break
}
// 当然可以用 for i := range ch {} 来获取通道多个数据
fmt.Println("ch1,data:", data)
}
}
func test1(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
}
// 手动关闭, 告诉接收方 放入数据完毕 (这样接收方不用知道通道里面有多少数据了, 可以直接读到通道里没有数据)
close(ch)
}
缓冲通道
非缓冲通道
chan, 只能存放一个数据, 发送和接收都是阻塞的。(效率低) 一次发送对应一个接收
缓冲通道
通道带了一个缓冲区, 发送的数据直到缓冲区满, 才会被阻塞; 接收同理, 只有缓冲区清空, 才会阻塞
chan 定义cap 通道的容量
package main
import (
"fmt"
"time"
)
// 缓冲通道 chan 设置cap
func main() {
// 非缓冲通道
ch1 := make(chan int)
fmt.Println(cap(ch1), len(ch1)) // 0 0
// 缓冲通道, 放入数据, 不会产生死锁, 不需要等待另外的线程去取数据
// 若缓冲区满了, 还没有进程去取通道里的数据; 若再放, 就会产生死锁
ch2 := make(chan int, 5)
fmt.Println(cap(ch2), len(ch2)) // 5 0
ch2 <- 1
ch2 <- 2
data := <-ch2
ch2 <- 3
ch2 <- 4
ch2 <- 5
fmt.Println(cap(ch2), len(ch2)) // 5 0
fmt.Println(data) // 1 这个是通过放数据的顺序依次取的
fmt.Println("==============")
ch3 := make(chan int, 5) // 这样就先往缓冲区里放数据 然后 取1放1 ...
go test(ch3)
for s := range ch3 {
time.Sleep(time.Second)
fmt.Println("main里面读取的数据", s)
}
fmt.Println("main-end")
}
func test(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
fmt.Println("子Goroutine放入的数据", i)
}
close(ch)
}
缓冲通道, 可以自定义其容量 (存放数据的量)
-
若缓冲区没满, 则可以继续存放; 若满了, 也会阻塞等待
-
若缓冲区空的, 读取也会等待; 如果缓冲区有多个数据, 会依次按照先进先出的规则进行读取
-
若缓冲区满了, 有≥2个线程进行读/写, 这个时候和普通chan一样, 一读一写
定向通道
双向通道
channel是用来实现goroutine通信的, 一个读, 一个写, 双向通道 (上面定义的是双向通道)
单向通道
ch2 := make(chan <- int, 1) // 只能写数据, 不能读
ch3 := make(<- chan int, 1) // 只能读数据, 不能写
使用场景:
参数定义. 指定函数去写, 不让其读取; 指定函数去读, 不让其写入. 相当于设定了权限
func WriteOnly(ch chan <- int){} // 只可以写
func ReadOnly(ch <-chan int) // 只可以读
Select
select 只能在通道中使用
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int, 2)
ch2 := make(chan int, 2)
go func() {
time.Sleep(time.Second * 2)
ch1 <- 100
}()
go func() {
time.Sleep(time.Second * 2)
ch2 <- 200
}()
// 读取chan里的数据
// select 和 switch 只是在通道中使用, case表达式需要的是一个通道结果
// 会一起进行匹配, 看谁先匹配到, 匹配到后 其余case子句就不匹配了
select {
case num1 := <-ch1:
fmt.Println(num1)
case num2 := <-ch1:
fmt.Println(num2)
default:
fmt.Println("default")
}
}
- 每一个case语句都需要是一个通道的操作
- 所有chan操作都要有结果 (通道表达式是要求值)
- 如果任何的通道拿到了结果。它就立即执行该case, 其他就会被忽略
- 如果有多个case可以执行, select是随机选取一个执行, 其他的不会执行
- 如果存在default, 执行该语句; 如果不存在, 就会阻塞等待select执行某个case语句 (随即选取执行)
Timer 计时器
常与通道一起使用, 有一些用法在前面介绍 go 包中也有所记录
package main
import (
"fmt"
"time"
)
func main() {
// 创造一个定时器
timer := time.NewTimer(time.Second)
// Timer C <- chan Time
timeChan := timer.C // 时间通道 (定义定时器的时候, 存放的时间)
fmt.Println(<-timeChan) // 只能读
// 提前关闭, 定义定时器之后 会放入一个时间 直至消费了 -> 结束
time2 := time.NewTimer(time.Second * 2)
go func() {
//消费
<-time2.C
fmt.Println("end")
}()
// 手动结束定时器
time2.Stop()
}
常见用途:
- 定时发邮件
- 定时去存储数据库文件 sql
- 在xx时间段后实现一些事情
package main
import (
"fmt"
"time"
)
// 定时器应用
func main() {
time1 := time.After(time.Second * 1) // 在1s后放到通道中
fmt.Println(time.Now())
timer1 := <-time1
fmt.Println(timer1) // 这里是打印当前时间一秒之后的时间~
time2 := time.AfterFunc(time.Second, test)
fmt.Println(<-time2.C) // 只是读取, 还没有使用, 故终端会返回deadlock
}
func test() {
fmt.Println("test")
}