下面是一个示例,我们模拟消息队列的消费者、生产者:
package main import ( "fmt" "time" ) func Producer(c chan<- int) { for i := 0; i < 10; i++ { c <- i } } func Consumer1(c <-chan int) { for m := range c { fmt.Printf("oh, I get luckly num: %v\n", m) } } func Consumer2(c <-chan int) { for m := range c { fmt.Printf("oh, I get luckly num too: %v\n", m) } } func main() { c := make(chan int, 2) go Consumer1(c) go Consumer2(c) Producer(c) time.Sleep(time.Second) }对于生产者,我们希望通道是只写属性,而对于消费者则是只读属性,这样避免对通道进行错误的操作。当然,如果你将本例里消费者、生产者的通道单向属性去掉也是可以的,没什么问题:
func Producer(c chan int) {} func Consumer1(c chan int) {} func Consumer2(c chan int) {}事实上 channel 只读或只写都没有意义,所谓的单向 channel 其实只是方法里声明时用,如果后续代码里,向本来用于读channel里写入了数据,编译器会提示错误。
关闭通道读取一个已经关闭的通道会立即返回通道类型的零值,而写一个已经关闭的通道会抛异常。如果通道里的元素是整型的,读操作是不能通过返回值来确定通道是否关闭的。
1、如何安全的读通道,确保不是读取的已关闭通道的零值?
答案是使用for...range语法。当通道为空时,循环会阻塞;当通道关闭,循环会停止。通过循环停止,我们可以认为通道已经关闭。示例:
输出:
12、如何安全的写通道,确保不会写入已关闭的通道?
Go 语言并不存在一个内置函数可以判断出通道是否已经被关闭。确保通道写安全的最好方式是由负责写通道的协程自己来关闭通道,读通道的协程不要去关闭通道。
但是这个方法只能解决单写多读的场景。如果遇到多写单读的情况就有问题了:无法知道其它写协程什么时候写完,那么也就不能确定什么时候关闭通道。这个时候就得额外使用一个通道专门做这个事情。
我们可以使用内置的 sync.WaitGroup,它使用计数来等待指定事件完成:
package main import ( "fmt" "sync" "time" ) func main() { var ch = make(chan int, 8) //写协程 var wg = new(sync.WaitGroup) for i := 1; i <= 4; i++ { wg.Add(1) go func(num int, ch chan int, wg *sync.WaitGroup) { defer wg.Done() ch <- num ch <- num * 10 }(i, ch, wg) } //读 go func(ch chan int) { for num := range ch { fmt.Println(num) } }(ch) //Wait阻塞等待所有的写通道协程结束,待计数值变成零,Wait才会返回 wg.Wait() //安全的关闭通道 close(ch) //防止读取通道的协程还没有完毕 time.Sleep(time.Second) fmt.Println("finish") }输出:
3 30 2 20 1 10 4 40 finish 多路通道有时候还会遇到多个生产者,只要有一个生产者就绪,消费者就可以进行消费的情况。这个时候可以使用go语言提供的select 语句,它可以同时管理多个通道读写,如果所有通道都不能读写,它就整体阻塞,只要有一个通道可以读写,它就会继续。示例:
package main import ( "fmt" "time" ) func main() { var ch1 = make(chan int) var ch2 = make(chan int) fmt.Println(time.Now().Format("15:04:05")) go func(ch chan int) { time.Sleep(time.Second) ch <- 1 }(ch1) go func(ch chan int) { time.Sleep(time.Second * 2) ch <- 2 }(ch2) for { select { case v := <-ch1: fmt.Println(time.Now().Format("15:04:05") + ":来自ch1:", v) case v := <-ch2: fmt.Println(time.Now().Format("15:04:05") + ":来自ch2:", v) //default: //fmt.Println("channel is empty !") } } }输出:
13:39:56 13:39:57:来自ch1: 1 13:39:58:来自ch2: 2 fatal error: all goroutines are asleep - deadlock!默认select处于阻塞状态,1s后,子协程1完成写入,主协程读出了数据;接着子协程2完成写入,主协程读出了数据;接着主协程挂掉了,原因是主协程发现在等一个永远不会来的数据,这显然是没有结果的,干脆就直接退出了。