1 概述
通道是Golang提供的一种基本类型,它可以实现在协程之间的单向通信和双向通信、发送和接收数据、以及协程同步。
channel的本质是一个队列,遵循先进先出原则。channel是线程安全的,在任何给定时间,一个数据被设计为只有一个协程可以对其访问,不会发生数据竞争,多goroutine访问时,也不需要加锁,这些是有编译器在底层维护的。
2 分类
在Go中,通道有两种主要类型:
非缓冲通道:发送操作会一直阻塞,直到有一个goroutine接收到了这个消息;
带缓冲通道:发送操作不会被阻塞,直到通道内的消息数量超过了缓冲区的大小;
3 声明与初始化
3.1 声明
var chanName chan toTransDatatype
var funcChan chan func(int) error
var chanChan chan chan int
通道的数据类型为 chan,后面的 toTransDataType 表示 想要传递的数据类型,如 int、float、string、interface{}、结构体、指针、func()等,甚至 chan本身。
var m map[string] chan bool
上面的代码声明了一个 m 的 map,它的key为string类型,它的value 为 chan bool 类型
3.2 初始化
channelWithoutBuf := make(chan int) // 无缓冲通道
channelWithBuf := make(chan int, 3) // 有缓冲通道,缓冲区size为3,提前分配了内存
3.3 示例
package main
import "fmt"
func main() {
var a chan int
fmt.Println(fmt.Sprintf("a :%+v, type: %T", a, a))
b := make(chan int)
fmt.Println(fmt.Sprintf("b :%+v, type: %T", b, b))
c := make(chan int, 3)
fmt.Println(fmt.Sprintf("c :%+v, type: %T", c, c))
a = b
fmt.Println(fmt.Sprintf("new a :%+v, type: %T", a, a))
b = c
fmt.Println(fmt.Sprintf("new b :%+v, type: %T", b, b))
return
}
输出
a :<nil>, type: chan int
b :0x14000016540, type: chan int
c :0x14000100000, type: chan int
new a :0x14000016540, type: chan int
new b :0x14000100000, type: chan int
可以看到,通过 var 声明的通道,值为nil,相当于是一个空管道。
把 b 赋值给 a,此时 a 也就成了 b;同时,把 b 赋值给 c,b也会变成 c,说明初始化的不同类型通道之间是可以互相转化的。
4 操作
通道的操作符为 <-,语法是:
- chanName<- value:把value变量的值写入chanName通道;
- value := <-chanName:从chanName中取出数据,赋值给value变量;
我们来试试:
package main
import "fmt"
func main() {
var chanA chan int
chanA <- 1
data := <-chanA
fmt.Println(fmt.Sprintf("data: %+v, type:%T", data, data))
//close(chanA)
//// 由管道中读写数据,<-操作符是与最左边的chan优先结合的
// 向管道中写入一个数据,在此需要注意:向管道中写入数据通常会导致当前协程阻塞,直到有其他协程从这个管道中读取数据
}
输出
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send (nil chan)]:
main.main()
/Users/yiyunjie/go/src/Test/main.go:7 +0x28
Process finished with the exit code 2
可以看到,main协程非正常退出,所有的协程处于休眠状态 - 死锁!所谓死锁,就是双方都在等待对方释放锁。
在当前场景,就是两个协程一直在等待对方释放资源。而在这个例子中,只有一个main函数所在的协程,并没有新的协程参与,为什么会死锁呢?
原因在于 往一个无缓冲通道里写入数据时,发送操作会一直阻塞、等待返回,直到有一个协程接收到了这个通道里的数据,阻塞状态才会解除。而上面的例子,是直接在当前协程接收数据,而当前协程一直在阻塞,不能继续往下执行,所以接收数据的代码根本就没执行到。这样程序就一直在等待一个无法释放的锁,形成了死锁。Go的运行时系统检测到这种情况后,会抛出这个错误,并终止程序的执行。
要解决这个问题,有两种方法:将发送操作变成非阻塞的;保持发送操作为阻塞的,用另一个协程接收数据;
4.1 非阻塞发送
我们知道,往带缓冲通道里写入数据是非阻塞的,直到通道内的消息数量超过了缓冲区的大小。那我们就定义一个带缓冲通道试试:
package main
import (
"fmt"
)
func main() {
chanA := make(chan int, 3)
chanA <- 1
data := <-chanA
fmt.Println(fmt.Sprintf("data: %+v, type: %T", data, data))
return
}
输出
data: 1, type: int
没有问题,可以正常接收到数据,且类型相符。
4.2 用新协程接收非缓冲通道信息
由于往非缓冲通道发送数据是阻塞的,所以不能把接收数据的新协程写在发送操作之后,不然当前协程一直阻塞,新协程还是无法接受到数据。所以只能把接收动作放在发送之前。
package main
import (
"fmt"
"time"
)
func main() {
chanA := make(chan int)
go func() {
data := <-chanA
fmt.Println(fmt.Sprintf("data: %+v, type: %T", data, data))
}()
chanA <- 1
time.Sleep(10 * time.Millisecond)
return
}
输出
data: 1, type: int