Channel是Go中的一个核心类型,可以将其看成一个管道,通过它并发单元就可以发送或者接收数据进行通信(communication)。
Do not communicate by sharing memory; instead, share memory by communicating.
channel基础知识
创建channel
使用内建函数make创建channel:
unBufferChan := make(chan int) // 1 无缓冲的channel
bufferChan := make(chan int, N) // 2 带缓冲的channel
- 无缓冲: 发送和接收动作是同时发生的。如果goroutine读取channel(<-channel),则发送者(channel<-)会一直阻塞。
- 缓冲 channel 类似一个有容量的队列。当队列满的时候发送者会阻塞;当队列空的时候接收者会阻塞。
写出以下代码打印的结果:
func main() {
var x chan int
go func() {
x <- 1
}()
<-x
}
结果分析:往一个nil channel中发送数据会一直被阻塞,从一个nil channel中接收数据会一直被block,所以才会产生如下死锁的现象。
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive (nil chan)]:
main.main()
/Users/kltao/code/go/examples/channl/channel1.go:11 +0x60
goroutine 4 [chan send (nil chan)]:
main.main.func1(0x0)fatal error: all goroutines are asleep - deadlock!
结论:channl 使用要小心,使用前切记要初始化,初始化函数用make。
channel读写操作
ch := make(chan int, 10)
// 读操作
x <- ch
// 写操作
ch <- x
channel关闭
channel可以通过内建函数close()来关闭:
ch := make(chan int)
// 关闭
close(ch)
关闭channel注意事项:
- 重复关闭channel会导致panic。
- 向关闭的channel发送数据会panic。
- 从关闭的channel读数据不会panic,读取出channel中已有的数据之后再读就是channl类型的默认值,比如chan bool类型的channel关闭之后读取的值为false。
区分channl中读取的默认值还是channl中传输的值,采用ok_idiom方式:
ch := make(chan int, 10)
...
close(ch)
// ok-idiom
val, ok := <-ch
if ok == false {
// channel closed
}
channel使用
select
golang的select的功能和select 、poll、epoll类似,就是监听IO操作,当IO操作发生时触发响应的动作(select 的case里的语句只能是IO操作) 。select使用一般配合for循环使用。
示例1
ch1 := make (chan int, 1)
ch2 := make (chan int, 1)
...
select {
case <-ch1:
fmt.Println("ch1 pop one element")
case <-ch2:
fmt.Println("ch2 pop one element")
}
结果:
此示例里面 select 会一直等待等到某个 case 语句完成, 也就是等到成功从 ch1 或者 ch2 中读到数据。 则 select 语句结束。
示例2—使用select实现timeout机制
import "time"
import "fmt"
func main() {
c1 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
c1 <- "result 1"
}()
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout 1")
}
}
结果:
这个例子我们会在2秒后往channel c1中发送一个数据,但是select
设置为1秒超时,因此我们会打印出timeout 1
,而不是result 1
。
示例3—带有default的select
““go
ch1 := make (chan int, 1)
ch2 := make (chan int, 1)
select {
case <-ch1:
fmt.Println(“ch1 pop one element”)
case <-ch2:
fmt.Println(“ch2 pop one element”)
default:
fmt.Println(“default”)
}
““
结果:
ch1 和 ch2 都为空,所以 case1 和 case2 都不会读取成功。 则 select 执行 default 语句。
作用:
因为这个 default 特性, 我们可以使用 select 语句来检测 chan 是否已满。示例代码如下:
ch := make (chan int, 1)
ch <- 1
select {
case ch <- 2:
default:
fmt.Println("channel is full !")
}
//ch 插入 1 的时候已经满了, 当 ch 要插入 2 的时候,发现 ch 已经满了(case1 阻塞住), 则 select 执行 default 语句。 这样就可以实现对 channel 是否已满的检测, 而不是一直等待。
range channel
range channel 可以直接取到 channel 中的值。当我们使用 range 来操作 channel 的时候,一旦 channel 关闭,channel 内部数据读完之后循环自动结束。
func consumer(ch chan int) {
for x := range ch {
fmt.Println(x)
...
}
}
func producer(ch chan int) {
for _, v := range values {
ch <- v
}
}
单向channel
单向channel,顾名思义只能写或读的channel。但是在实际使用中用处不大,单向channel主要用于函数声明。比如:
func foo(ch chan<- int) <-chan int {...}
//foo 的形参是一个只能写的 channel,那么就表示函数 foo 只会对 ch 进行写,当然你传入的参数可以是个普通 channel。foo 的返回值是一个只能读的 channel,那么表示 foo 的返回值规范用法就是只能读取。
单向channel在功能上和普通的channel并没有太大区别。但是使用单向channel编程体现一种非常优秀的编程范式:convention over configuration(约定由于配置)。
channel的典型用法
同步goroutine
chan是go里的第一对象,所以可以把chan传入chan中。具体示例如下:
““go
func main() {
g := make(chan int)
quit := make(chan chan bool)
go Channel(g, quit)
for i := 0; i < 5; i++ {
g<-i
}
wait := make(chan bool)
quit <-wait
<-wait //等Channel函数中传入的bool类型的chan有值写入的时候才会继续执行
fmt.Println(“Main Quit”)
}
func Channel(g chan int, quit chan chan bool) {
for {
select {
case i := <-g:
fmt.Println(i)
case c := <-quit: //接收main函数传递的bool类型的channel
c <- true //向其中写入值true
fmt.Println(“channel function Quit”)
return
}
}
}
/* output*/
0
1
2
3
4
B Quit
Main Quit
““
基于channel、gorutine实现工作池
worker工作池负责处理任务jobs,然后将加工后的结果写入到result,所以此处需要两个通道。jobs负责任务的传递,results负责结果的传输。这里启动3个worker,初始是阻塞的,因为还没有任务传递。然后传递9个任务到jobs中并关闭通道表示全部任务发送完毕。最后调用sumResult()统计任务返回的结果。
func worker(id int, jobs chan int, results chan int) {
for j := range jobs {
fmt.Println("worker", id, "process job", j)
time.Sleep(time.Second)
results <- j * 2
}
}
func sumResult(results chan int) {
var sum int
for a := 1; a <= 9; a++ {
s := <-results
sum += s
fmt.Println("sum", sum, "s", s)
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
sumResult(results)
}
channel 问题
问题: [Is it OK to leave a channel open?
解答:
It's OK to leave a Go channel open forever and never close it. When the channel is no longer used, it will be garbage collected.
Note that it is only necessary to close a channel if the receiver is looking for a close. Closing the channel is a control signal on the channel indicating that no more data follows.