Go_channel
通道可以被认为是Goroutines通信的管道。类似于管道中的水从一端到另一端的流动,数据可以从一端发送到另一端,通过通道接收。
在前面讲Go语言的并发时候,我们就说过,当多个Goroutine想实现共享数据的时候,虽然也提供了传统的同步机制,但是Go语言强烈建议的是使用Channel通道来实现Goroutines之间的通信。
“不要通过共享内存来通信,而应该通过通信来共享内存” 这是一句风靡golang社区的经典语
Go语言中,要传递某个数据给另一个goroutine(协程),可以把这个数据封装成一个对象,然后把这个对象的指针传入某个channel中,另外一个goroutine从这个channel中读出这个指针,并处理其指向的内存对象。Go从语言层面保证同一个时间只有一个goroutine能够访问channel里面的数据,为开发者提供了一种优雅简单的工具,所以Go的做法就是使用channel来通信,通过通信来传递内存数据,使得内存数据在不同的goroutine中传递,而不是使用共享内存来通信。
一、 什么是通道
1.1 通道的概念
通道是什么,通道就是goroutine之间的通道。它可以让goroutine之间相互通信。
每个通道都有与其相关的类型。该类型是通道允许传输的数据类型。(通道的零值为nil。nil通道没有任何用处,因此通道必须使用类似于map和切片的方法来定义。)
1.2 通道的声明
声明一个通道和定义一个变量的语法一样:
//声明通道 var 通道名 chan 数据类型 //创建通道:如果通道为nil(就是不存在),就需要先创建通道 通道名 = make(chan 数据类型)
也可以简短的声明:
a := make(chan int)
1.3 channel的数据类型
channel是引用类型的数据,在作为参数传递的时候,传递的是内存地址。
package main import "fmt" func main() { /* channel,通道,先进先出 */ var a chan int fmt.Printf("%T,%v\n", a, a) //chan int,<nil> if a == nil { fmt.Println("channel是nil的,不能使用,需要先创建通道。。") a = make(chan int) fmt.Println(a) //0xc000046060 //打印出来的是地址,channel是引用类型的数据 } test1(a) } func test1(ch chan int) { //传递的是地址 fmt.Printf("%T,%v\n", ch, ch) //chan int,0xc000046060 }
1.4 通道的注意点
Channel通道在使用的时候,有以下几个注意点:
1.用于goroutine,传递消息的。
2.通道,每个都有相关联的数据类型, nil chan,不能使用,类似于nil map,不能直接存储键值对
3.使用通道传递数据:<-
chan <- data,发送数据到通道。向通道中写数据 data <- chan,从通道中获取数据。从通道中读数据
4.阻塞:
发送数据:chan <- data,阻塞的,直到另一条goroutine,读取数据来解除阻塞 读取数据:data <- chan,也是阻塞的。直到另一条goroutine,写出数据解除阻塞。
5.本身channel就是同步的,意味着同一时间,只能有一条goroutine来操作。
最后:通道是goroutine之间的连接,所以通道的发送和接收必须处在不同的goroutine中。
二、通道的使用语法
2.1 发送和接收
发送和接收的语法:
data := <- a // read from channel a a <- data // write to channel a
在通道上箭头的方向指定数据是发送还是接收。
另外:
v, ok := <- a //从一个channel中读取
2.2 发送和接收默认是阻塞的
一个通道发送和接收数据,默认是阻塞的。当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个Goroutine从该通道读取数据。相对地,当从通道读取数据时,读取被阻塞,直到一个Goroutine将数据写入该通道。
这些通道的特性是帮助Goroutines有效地进行通信,而无需像使用其他编程语言中非常常见的显式锁或条件变量。
示例代码:
package main import ( "fmt" "time" ) func main() { ch1 := make(chan int) done := make(chan bool) // 通道 go func() { fmt.Println("子goroutine执行。。。") time.Sleep(3 * time.Second) data := <-ch1 // 从通道中读取数据 fmt.Println("data:", data) done <- true }() // 向通道中写数据。。 time.Sleep(5 * time.Second) ch1 <- 100 <-done fmt.Println("main。。over") } //子goroutine执行。。。 //data: 100 //main。。over
2.3 死锁
使用通道时要考虑的一个重要因素是死锁。如果Goroutine在一个通道上发送数据,那么预计其他的Goroutine应该接收数据。如果这种情况不发生,那么程序将在运行时出现死锁。
类似地,如果Goroutine正在等待从通道接收数据,那么另一些Goroutine将会在该通道上写入数据,否则程序将会死锁。
package main func main() { ch := make(chan int) ch <- 5 } //fatal error: all goroutines are asleep - deadlock!
在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等。Go语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言也差不多。 除了这些基本的同步手段,Go语言还提供了一种新的同步机制: Channel,它在Go语言中是一个像int, float32等的基本类型,一个channel可以认为是一个能够在多个Goroutine之间传递某一类型的数据的管道。Go中的channel无论是实现机制还是使用场景都和Java中的BlockingQueue很接近。
三、 关闭通道
发送者可以通过关闭信道,来通知接收方不会有更多的数据被发送到channel上。
close(ch)
接收者可以在接收来自通道的数据时使用额外的变量来检查通道是否已经关闭。
语法结构:
v, ok := <- ch
类似map操作,存储key,value键值对
v,ok := map[key] //根据key从map中获取value,如果key存在, v就是对应的数据,如果key不存在,v是默认值
在上面的语句中,如果ok的值是true,表示成功的从通道中读取了一个数据value。如果ok是false,这意味着我们正在从一个封闭的通道读取数据。从闭通道读取的值将是通道类型的零值。
例如,如果通道是一个int通道,那么从封闭通道接收的值将为0。
package main import ( "fmt" "time" ) func main() { /* 关闭通道:close(ch) 子goroutine:写出10个数据 每写一个,阻塞一次,主goroutine读取一次,解除阻塞 主goroutine,读取数据 每次读取数据,阻塞一次,子goroutine,写出一个,解除阻塞 */ ch1 := make(chan int) go sendData(ch1) //读取通道的数据 for{ time.Sleep(1*time.Second) v, ok := <- ch1 // 最后一次读取 if !ok{ fmt.Println("已经读取了所有的数据。。",ok,v) break } fmt.Println("读取的数据:",v,ok) } fmt.Println("main..over...") } func sendData(ch1 chan int){ //发送方:10条数据 for i:=0;i<10;i++{ ch1 <- i //将i写入到通道中 // 0 1 .. 9 } close(ch1) //将ch1通道关闭 } 我们可以循环从通道上获取数据,直到通道关闭。for循环的for range形式可用于从通道接收值,直到它关闭为止。 package main import ( "fmt" "time" ) func main() { /* 通过range访问通道 */ ch1 := make(chan int) go sendData(ch1) //for循环的for range,来访问通道 for v := range ch1{ // v <- ch1 fmt.Println("读取数据:",v) } fmt.Println("main..over...") } func sendData(ch1 chan int){ for i:=0;i<10;i++{ time.Sleep(1* time.Second) ch1 <- i // 0 1...9 } close(ch1)//通知对方,通道关闭,对方才能结束循环,不读了 }