channel
一.基本语法
c := make(chan bool) //创建一个无缓冲的bool型Channel c <- x //向一个Channel发送一个值 <- c //从一个Channel中接收一个值 x = <- c //从Channel c接收一个值并将其存储到x中 x, ok = <- c //从Channel接收一个值,如果channel关闭了或没有数据,那么ok将被置为false ch chan int //可读写 ch1 chan<- int //ch1只能写 ch2 <-chan int //ch2只能读 channel是类型相关的,也就是一个channel只能传递一种类型
二.为什么要使用channel
goroutine是Go语言中的轻量级线程实现,由Go运行时(runtime)管理.
先看一个例子:
func Sub(i int) { fmt.Println("from sub func", i) } func main() { for i := 0; i < 5; i++ { Sub(i) } fmt.Println("from main") }
这个例子做了一件事情,在main函数中串行执行了5次Sub函数.
如果我们需要Sub函数能够并发的执行,我们加个go,将每一个Sub函数放在goroutine中去(main函数其实也是个goroutine),代码如下所示:
func Sub(i int) { fmt.Println("from sub func", i) } func main() { for i := 0; i < 5; i++ { go Sub(i) } fmt.Println("from main") }
编译执行,你会发现只打印出了from main,Sub函数中字符并没有打印出来.这是因为主函数main启动了5个Sub函数后,并没有等待它们完成即退出了!这显然不是我们要的结果,我们使用go提供的消息通信机制channel来重构代码,保证Sub函数执行完成后,主函数再退出!
三.无缓存channel(信道,这个翻译较为接近其英文表达的意思)
channel(信道)是什么,简单说,是goroutine之间互相通讯的东西。类似我们Unix上的管道(可以在进程间传递消息), 用来goroutine之间发消息和接收消息。其实,就是在做goroutine之间的内存共享。
func Sub(ch chan int) { for i := 0; i < 5; i++ { fmt.Println("from sub func", i) } ch <- 1 // 向channel存消息,如果没有其他goroutine取走数据,那么挂起Sub } func main() { ch := make(chan int) // 创建了一个无缓存的channel go Sub(ch) // 开启一个goroutine来执行Sub fmt.Println("from main") <-ch // 从channel取消息,如果没有写入消息,挂起main }
无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine.
所以上面代码的执行流程是:
创建ch信道
新开goroutine来执行Sub,但因为ch信道中写入的消息没有被取走,Sub挂起.
from main被打印出来.
<-ch 挂起了main,直到取到数据,这就保证了Sub完成后,main才结束.
所以无缓存信道,存入数据必须取走,只存不取或取空的数据,都将导致死锁.
以下情形都是死锁:
//取不存在的消息: ch := make(chan int) <-ch //只写不取: ch := make(chan int) ch <- 1 //多个channel,ch1等待ch2的消息,但ch2没有消息写入: ch1 := make(chan int) ch2 := make(chan int) ch1 <- <-ch2 <-ch1
四.带缓存的channel
无缓存信道只负责流通消息,任何对该信道的读和写,都阻塞信道.
带缓存的channel,不仅可以流通数据,还可以缓存数据,只有达到缓存最大数目后,也就是缓存满了后,才阻塞信道,这听起来很像队列(Queue).其实,缓冲信道是先进先出的,我们可以把缓冲信道看作为一个线程安全的队列
ch := make(chan int, 2) // 带缓存channel,缓存数目2个,放入2个数据,不会挂起,只有放入第3个的时候才挂起当前goroutine
示例代码:
func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) }
对ch的写入,因为没有超过缓存数目3,所以不会阻塞;对ch取数据,将按先进先出,依次输出1 2 3
你会发现,带缓存的信道,取数据还是挺麻烦的.我们用for range来取数据
func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 4 for v := range ch { fmt.Println(v) } }
报出deadlock,原因是ch没有关闭的情形下,range一直在读取.加一个判断:
func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 4 for v := range ch { fmt.Println(v) if len(ch) <= 0 { break } } }
我们判断了ch中有没有数据,没有数据就跳出循环.可以正常输出.当然我们也可以显式关闭信道.
func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 4 close(ch) // 关闭信道 for v := range ch { fmt.Println(v) } }
五.执行多个goroutine
1.无缓存
const MAX = 1000 var ch chan int func Sub(i int) { fmt.Println(i) ch <- 1 } func main() { ch = make(chan int) for i := 0; i < MAX; i++ { go Sub(i) } for i := 0; i < MAX; i++ { <-ch } }
2.带缓存
const MAX = 1000 var ch chan int func Sub(i int) { fmt.Println(i) ch <- 1 } func main() { ch = make(chan int, MAX) for i := 0; i < MAX; i++ { go Sub(i) } for i := 0; i < MAX; i++ { <-ch } }
两者效果相同,不同点在于:
无缓冲的信道是一批数据一个一个的流进流出
缓冲信道则是一个一个存储,然后一起流出去
六.单向channel
顾名思义,单向channel只能用于发送或者接收数据。channel本身必然是同时支持读写的,否则根本没法用。假如一个channel真的只能读,那么肯定只会是空的,因为你没机会往里面写数据。同理,如果一个channel只允许写,即使写进去了,也没有丝毫意义,因为没有机会读取里面的数据。所谓的单向channel概念,其实只是对channel的一种使用限制。
ch1 chan<- int //ch1只能写
ch2 <-chan int //ch2只能读
示例: func Parse(ch <-chan int) { for value := range ch { fmt.Println("Parsing value", value) } }