8.3 Go channel
在Go语言中,关键字go
的引入使得Go语言并发编程更加简单而优雅,但是并发编程的复杂性,以及时刻关注并发编程容易出现的问题需要时刻警惕。
并发编程的难度在于协调
,然而协调就必须要交流
,那么并发单元之间的通信
是最大的问题。
之前说了在程序中两种并发通信模型:共享数据和消息
。
共享数据
是指多个并发单元
分别保持对同一个数据
的引用,实现对数据的共享。
共享数据
可能是内存数据块、磁盘文件、网络数据等。
Go语言既然一并发为核心,它提供了另一种通信模型,以消息机制而非共享内存作为通信方式
。
消息机制:每个并发单元的输入和输入只有一种。
1.1. channel介绍
Go语言提供的消息通信机制被称为channel
。
channel是go语言在语言级别提供的goroutine间的通信方式。 channel是有类型的,一种channel只能传递一种类型值,这个类型在声明channel时定义。
1.channel本质是一个数据结构(队列)
2.channel数据遵循FIFO
,first in first out。
3.channel本身是线程安全的,多个goroutine访问时不需要加锁。
4.一个string类型的channel只能放入string类型数据。
1.2. 定义channel
基本语法: var 管道名 chan 管道类型 如: var intChan chan int var strChan chan string var stuChan chan Student //结构体类型channel var mapChan chan map[string]string //map类型channel var boolChan chan bool //布尔类型channel
注意:
channel是引用类型 channel必须make初始化后方可使用
1.3. channel使用
package main import "fmt" func main() { //初始化一个可以存放3个int类型数据的管道 //创建方式一 //var intChan chan int //intChan = make(chan int, 3) //创建方式二,简短声明创建,如果不make初始化,默认chan是nil intChan := make(chan int, 3) //查看下channel里面有什么 fmt.Printf("intChan值:%v intChan地址:%p\n", intChan, &intChan) fmt.Printf("intChan长度:%v 容量%v\n", len(intChan), cap(intChan)) //向管道写入数据,只能写入int类型 intChan <- 10 //放入第二个数据 num := 999 intChan <- num //放入第三个数据 intChan <- 666 //注意我们初始化chan的容量限制在3,数据不能超过chan的容量 fmt.Printf("intChan长度:%v 容量%v\n", len(intChan), cap(intChan)) //从管道中读取数据 //定义一个变量接收chan的数据,注意先进先出的规律 res := <-intChan // res2 := <-intChan fmt.Println(res, res2) fmt.Printf("intChan长度:%v 容量%v\n", len(intChan), cap(intChan)) //数据遵循,有多少读多少,读空的channel也报错 res3 := <-intChan fmt.Println(res3) fmt.Printf("intChan长度:%v 容量%v\n", len(intChan), cap(intChan)) }
1.4. channel注意事项
1.channel只能存放声明的数据类型
2.channel数据放满了,不得再放入,否则panic报错,死锁
3.取出一个channel数据,少了一个坑,那可以再放入一个数据
4.数据取空后,不得在取,否则panic报错
1.5. channel存放数据类型
1.map类型channel
package main import "fmt" func main() { mapChan := make(chan map[string]string, 10) //初始化创建map类型的chan,可以放入map数据 myMap := make(map[string]string, 10) //初始化创建map myMap["姓名"] = "黑旋风" myMap["年纪"] = "28" mymap2 := make(map[string]string, 10) //初始化创建第二个map mymap2["姓名"] = "小妖精" mapChan <- myMap mapChan <- mymap2 fmt.Printf("值:%v 长度:%v 容量:%v\n", mapChan, len(mapChan), cap(mapChan)) }
2.结构体类型channel
package main import "fmt" type Student struct { Name string Age int } func main() { //创建管道 structChan := make(chan Student,2) //创建结构体对象 s1 := Student{"艾利克斯", 18} s2 := Student{"银角大王吧", 19} //数据放入管道 structChan <- s1 structChan <- s2 //取出管道数据 stu1 := <-structChan stu2 := <-structChan fmt.Println(stu1, stu2) }
3.存放指针数据的管道
package main import "fmt" type Student struct { Name string Age int } func main() { //创建管道 stuChan := make(chan *Student, 10) //创建结构体对象 s1 := Student{"王二狗", 18} s2 := Student{"王八犊子", 19} //传入结构体对象到管道 stuChan <- &s1 stuChan <- &s2 //取出管道的值 stu1 := <-stuChan stu2 := <-stuChan fmt.Printf("%v %v\n", stu1, stu2) }
4.创建可以接收任意数据类型
的channel,注意,空接口
可以存放任意数据类型。
package main import "fmt" type Student struct { Name string Age int } func main() { allChan := make(chan interface{}, 10) allChan <- 123 allChan <- "我是字符串" res1 := <-allChan res2 := <-allChan fmt.Printf("res1值:%v 类型:%T\n", res1, res1) fmt.Printf("res2值:%v 类型:%T\n", res2, res2) //定义结构体 s1 := Student{"王麻子", 11} allChan <- s1 //取出结构体,查看类型 newStu := <-allChan fmt.Printf("%T %v\n", newStu, newStu) //newStu此时无法直接使用,必须类型断言后使用 r := newStu.(Student) //断言它是结构体类型 fmt.Printf("r值:%v 类型:%T\n", r, r) }
1.6. channel缓冲区
初始化channel的方式是make
函数。
ch :=make(chan int) //无缓冲区的管道 ch2:=make(chan int,0)//无缓冲区的管道 ch3:=make(chan int,10)//有缓冲区的管道,且容量是10
如图所示:
两个协程,传递数据,无缓存的管道,就是不可以保存数据的通道。
无缓冲的管道:在管道接受数据前,不保存任何值。 无缓存的管道:这种类型的管道要求两个goroutine同时接、收值。 如果goroutine没同时准备好,会导致有一方阻塞状态。 这种管道进行接、收值的行为,本身是同步状态。
无缓冲管道案例
package main import ( "fmt" "time" ) //运动员1 func write(ch chan int) { for i := 0; i < 10; i++ { ch <- i fmt.Printf("棒子%d号来了\n", i) //每秒塞一个棒子 time.Sleep(time.Second) fmt.Println("-------------------") } } //运动员2 func read(ch chan int) { for v := range ch { fmt.Printf("拿到了棒子%d号\n", v) } } func main() { intChan := make(chan int, 0) go write(intChan) go read(intChan) time.Sleep(10 * time.Second) }
有缓冲的管道
有缓冲的管道:在被接收前能存储一个或多个值的通道。 这种管道不要求goroutine必须同时发送和接收。 只有管道中没有要接收的值,接收动作才会阻塞。 只有管道塞满了数据,发送动作才会阻塞。
示意图
package main import ( "fmt" "time" ) //运动员1 func write(ch chan int) { for i := 0; i < 10; i++ { ch <- i fmt.Printf("棒子%d号来了\n", i) //每秒塞一个棒子 fmt.Println("-------------------") } } //运动员2 func read(ch chan int) { for v := range ch { fmt.Printf("拿到了棒子%d号\n", v) } } func main() { intChan := make(chan int, 5) go write(intChan) go read(intChan) time.Sleep(2 * time.Second) }
1.7. 关闭channel
使用内置函数close可以关闭channel。
channel关闭后,就不能再向channel写入数据了,但是仍然可以从channel读取数据。
package main import "fmt" func main() { intChan := make(chan int, 3) intChan <- 123 intChan <- 456 close(intChan) //通道关闭,游客禁止入内,数据也无法写入 //intChan<-123 //此处已经不能加载 panic: send on closed channel fmt.Println("管道已经关闭,在管道中的游客请尽快出来") n1 := <-intChan n2 := <-intChan fmt.Println(n1, n2) }
遍历channel的方式
package main import "fmt" func main() { intChan := make(chan int, 100) for i := 0; i < 100; i++ { intChan <- i * i //循环写入一百个数据 } //遍历方式1,取出管道值 //for i := 0; i < len(intChan); i++ { // fmt.Println(i) //} //遍历方式2,for range,参数只有一个 //在遍历管道时,必须先关闭,否则死锁报错 close(intChan) for v := range intChan { fmt.Printf("类型:%T 值:%d\n", v, v) } }
1.8. 单向channel
默认情况下,channel是双向的,既可以写入数据,也可以读取数据。
但是有些场景下,管道当做参数传递,且希望仅仅是单向使用(只读,只写),此时可以使用单向管道。
语法
var ch1 chan int //普通管道 var ch2 chan <- string //只能写入string类型的数据 var ch3 <-chan int //只能读取int类型的数据
提示
chan <- 表示数据写入channel <-chan 表示读取channel中的数据
示例
1.8.1. 生产消费者模型
package main import ( "fmt" ) func Producer(baozi chan<- int) { for i := 0; i < 10; i++ { baozi <- i //把包子放到管道 fmt.Printf("三全食品,生产了有毒包子%d号\n", i) } //每天就只卖10个包子,卖完为止 close(baozi) } func Consumer(baozi <-chan int) { for baozi := range baozi { fmt.Printf("消费者吃了包子%d号.....\n", baozi) } } func main() { ch := make(chan int, 10) //生产者 go Producer(ch) //消费者 Consumer(ch) }
执行结果图
案例2
package main import "fmt" //只能写入数据 func sendData(sendch chan<- int) { sendch <- 10 //只写模式下,不能读取操作 //invalid operation: <-sendch (receive from send-only type chan<- int) //<-sendch } //只能读取数据 func readData(sendch <-chan int) { //sendch <- 10 data := <-sendch fmt.Println(data) } func main() { chnl := make(chan int) go sendData(chnl) //开启协程,写入数据到管道 readData(chnl) }