这这这本来是要昨天发的,昨天回家光顾着收拾了所以,完结篇来的稍晚一步。
这篇总结是紧接着Day13的并发编程的,所以看这篇总结之前,大家可以先去复习下前面的文章。
01
什么是Channel?
Go官方博客上的一篇文章:Go并发模式:管道和取消
https://blog.golang.org/pipelines
上面这篇文章大家可以先看下,介绍了如何使用Go来编写并发程序,并按照程序的演化顺序,介绍了不同模式遇到的问题以及解决的问题。主要解释了用管道模式链接不同的线程,以及如何在某个线程取消工作时,保证所有线程以及管道资源的正常回收。
之前我们讲了Go goroutine,知道Go 语言的并发通过 goroutine 特性完成。goroutine 类似于线程,但是可以根据需要创建多个 goroutine 并发工作。goroutine 是由 Go 语言的运行时调度完成,而线程是由操作系统调度完成。那我们今天要讲的Channel其实就是goroutine之间的通信机制,一个 channels 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channels 可发送数据的类型。我们可以用一张图来简单描述一下。
02
Channel的一些特性
Channel的本质是一个队列
Channel是线程安全的, 也就是自带锁定功能
遵循先入先出(First In First Out)的规则,保证收发数据的顺序
在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据
管道和切片/字典一样,必须创建后才能使用,否则会报错
Channel和切片还有字典一样, 是引用类型,是地址传递
03
Channel的声明
通道本身需要一个类型进行修饰,就像切片类型需要标识元素类型。通道的元素类型就是在其内部传输的数据类型,声明如下:
var 通道变量 chan 通道类型
chan类型的空值是nil,声明需要配合make后才能使用,语法如下
通道实例 := make(chan 数据类型,chan容量)
容量可以不声明,后面会详细讲解
ch1 := make(chan int) // 创建一个整型类型的通道ch2 := make(chan interface{}) // 创建一个空接口类型的通道, 可以存放任意格式type Equip struct{ /* 一些字段 */ }ch2 := make(chan *Equip) // 创建Equip指针类型的通道, 可以存放*Equip
04
Channel的使用
通道创建后,就可以使用通道进行发送和接收操作。
channel通过操作符
channel value //取出接收数据并将其丢弃x := //从channel中接收数据,并赋值给xx, ok := //功能同上,同时检查通道是否已关闭或者是否为空
默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得goroutine同步变的更加的简单,而不需要显式的lock。我们举个例子:
package mainimport "fmt"func main() { /* 1.什么是管道: 管道就是一个队列, 具备先进先出的原则 是线程安全的, 也就是自带锁定功能 2.管道作用: 在Go语言的协程中, 一般都使用管道来保证多个协程的同步, 或者多个协程之间的通讯 3.如何声明一个管道, 和如何创建一个管道 管道在Go语言中和切片/字典一样也是一种数据类型 管道和切片/字典非常相似, 都可以用来存储数据, 都需要make之后才能使用 3.1管道声明格式: var 变量名称 chan 数据类型 var myCh chan int 如上代码的含义: 声明一个名称叫做myCh的管道变量, 管道中可以存储int类型的数据 3.2管道的创建: make(chan 数据类型, 容量) myCh = make(chan int, 3); 路上代码的含义: 创建一个容量为3, 并且可以保存int类型数据的管道 4.管道的使用 4.1如何往管道中存储(写入)数据? myCh 4.2如何从管道中获取(读取)数据? 对管道的操作是IO操作 例如: 过去的往文件中写入或者读取数据, 也是IO操作 例如: 过去的往屏幕上输出内容, 或者从屏幕获取内容, 也是IO操作 stdin / stdout / stderr 注意点: 和切片不同, 在切片中make函数的第二个参数表示的切片的长度(已经存储了多少个数据), 而第三个参数才是指定切片的容量 但是在管道中, make函数的第二个参数就是指定管道的容量, 默认长度就是0 */ //1.定义一个管道 //var myChan chan int //2.使用make创建管道 myChan := make(chan int, 3) //3.往管道中存储数据 myChan<-1 myChan<-2 myChan<-3 //从管道中取出数据 fmt.Println( fmt.Println( fmt.Println( //定义一个管道 var myChan chan int //直接使用管道 //注意点: 会报错,管道定义完成后不创建是无法直接使用的 //myChan //fmt.Println( //创建管道 myChan = make(chan int, 3) //只要往管道中写入了数据, 那么len就会增加 myChan 2 //fmt.Println("len = ", len(myChan), "cap = ", cap(myChan)) myChan 4 //fmt.Println("len = ", len(myChan), "cap = ", cap(myChan)) myChan 6 //fmt.Println("len = ", len(myChan), "cap = ", cap(myChan)) //注意点: 如果len等于cap, 那么就不能往管道中再写入数据了, 否则会报错 //myChan //管道未写入数据,使用管道去取数据会报错 //从管道中取数据,len会减少 // fmt.Println( fmt.Println("len=",len(myChan),"cap = ", cap(myChan)) fmt.Println( fmt.Println("len=",len(myChan),"cap = ", cap(myChan)) fmt.Println( fmt.Println("len=",len(myChan),"cap = ", cap(myChan)) //注意点: 取数据个数也不可以超出写入的数据个数,否则会报错 //fmt.Println(}
05
Channel的遍历
两种方式:
for...range循环
for循环,for死循环
package mainimport "fmt"func main() { /* 管道的遍历: 可以使用for循环, 也可以使用 for range循环, 以及死循环来遍历 但是更推荐使用后两者 因为在企业开发中, 有可能我们不知道管道中具体有多少条数据, 所以如果利用for循环来遍历, 那么无法确定遍历的次数, 并且如果遍历的次数太多, 还会报错 */ //创建一个管道 myChan := make(chan int, 3) //往管道中写入数据 myChan 2 myChan 4 myChan 6 //注意点: 如果不关闭管道,遍历管道会报错 close(myChan) //第一种方法遍历管道 //for value := range myChan { // fmt.Println(value) //} //第二种方法遍历管道 //注意点: 如果被遍历的管道没有关闭, 那么会报错 // 如果管道没有被关闭, 那么会将true返回给ok, 否则会将false返回给Ok for { if v,ok := fmt.Println(v) fmt.Println(ok) }else { break } } //注意点: 管道关闭后无法往里面写入数据,会报错,但是可以读取数据不会报错 myChan := make(chan int,3) close(myChan) //往管道中写入数据 //myChan //myChan //myChan //从管道中读取数据 }
还是把需要注意的地方再列出来强调一下:
使用range循环管道,如果管道未关闭会引发deadlock错误。
如果采用for死循环已经关闭的管道,当管道没有数据时候,读取的数据会是管道的默认值,并且循环不会退出。
06
两种Channel
1.带缓冲区channe和不带缓冲区channel
带缓冲区channel:定义声明时候制定了缓冲区大小(长度),可以保存多个数据。
不带缓冲区channel:只能存一个数据,并且只有当该数据被取出时候才能存下一个数据。
ch := make(chan int) //不带缓冲区ch := make(chan int ,10) //带缓冲区
// 不带缓冲区的channel示例package mainimport "fmt"func test(c chan int) { for i := 0; i < 10; i++ { fmt.Println("send ", i) c }}func main() { ch := make(chan int) go test(ch) for j := 0; j < 10; j++ { fmt.Println("get ", }}
输出结果:
2.只读channel和只写channel
默认情况下所有的管道都是双向的管道(可读可写),在日常开发中, 我们可能会需要将管道作为函数的参数, 并且还需要限制函数中如何使用管道,,那么这个时候我们就可能会使用单向管道。
双向管道和正常的声明管道方法一样。
单向管道声明示例如下:
var myCh chanint; 只写的管道var myCh chan
同样我们需要注意,双向管道可以转换为单向的管道,但是单向的管道不能转换为双向的管道。
package mainimport "fmt"func main() { /* 双向格式: var myCh chan int; myCh = make(chan int, 5) myCh = make(chan int) 单向格式: var myCh chan var myCh */ //定义一个双向管道 myChan := make(chan int,3) //定义一个只读的单向管道 var myChan1 chan //定义一个只写的单向管道 //var myChan2 chan //将双向管道赋给只读的单向管道 myChan1 = myChan fmt.Println(myChan1) //将双向管道赋给只写的单向管道 myChan2 = myChan fmt.Println(myChan2) //将单向管道赋给双向管道 //会报错 //myChan = myChan1 //fmt.Println(myChan)}
07
select语句
早期的select函数是用来监控一系列的文件句柄,一旦其中一个文件句柄发生IO操作,该select调用就会被返回。golang在语言级别直接支持select,用于处理异步IO问题。
select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。
以下是select语句的语法格式:
select { case communication clause : statement(s); case communication clause : statement(s); /* 你可以定义任意数量的 case */ default : /* 可选 */ statement(s);}
几个注意的点:
每个 case 都必须是一个通信
所有 channel 表达式都会被求值
所有被发送的表达式都会被求值
如果任意某个通信可以进行,它就执行,其他被忽略。
如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。
否则:如果有 default 子句,则执行该语句。
如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。
timeout := make (chan bool, 1)ch := make(chan int)select { case case fmt.Println("timeout!") default: fmt.Println("default case is running")}
可以看出,ch初始化后,第一个case读取失败,第二个case同样失败,因为channel中无数据,直接跳至default执行并返回。注意,如果没有default,select 会一直等待等到某个 case 语句完成, 也就是等到成功从 ch 或者 timeout 中读到数据,否则一直阻塞。
基于这种机制,可以使用select实现channel读取超时的机制
package mainimport ( "fmt" "time")func main() { timeout := make(chan bool, 1) go func() { time.Sleep(3e9) // sleep 3 seconds timeout }() ch := make(chan int) select { case case fmt.Println("timeout!") }}
注意这里一定不能用default,否则3s超时还未到直接执行default,case2便不会执行,超时机制便不会实现。timeout会在3s超时后读取到数据。
使用select判断channel是否存满
ch1 := make(chan int, 1)ch2 := make(chan int, 1)select { case fmt.Println("ch1 pop one element") case fmt.Println("ch2 pop one element") default: fmt.Println("default")}
如果case1、case2均未执行,则说明ch1和ch2已满
选择循环
当多个channel需要读取数据的时候,就必须使用 for+select。例如:下面例子需要从两个channel中读取数据,当从channel1中数据读取完毕后,会像signal channel中输入stop,此时终止for+select。
func f1(c chan int, s chan string) { for i := 0; i < 10; i++ { time.Sleep(time.Second) c } s "stop"}func f2(c chan int, s chan string) { for i := 20; i >= 0; i-- { time.Sleep(time.Second) c } s "stop"}func main() { c1 := make(chan int) c2 := make(chan int) signal := make(chan string, 10) go f1(c1, signal) go f2(c2, signal)LOOP: for { select { case data := fmt.Println("c1 data is ", data) case data := fmt.Println("c2 data is ", data) case data := fmt.Println("signal is ", data) break LOOP } }}
☆ END ☆
14天组队学习任务就结束啦,但是学习尚未结束。现在各个大厂都进入了多语言时代,14天的学习对我来说更多的是一次知识回顾,从中确实有很多不一样的体会,接下来会进入更高阶的学习阶段。离新年还有一个月左右的时间,希望自己可以利用好这段时间继续加油。我会尽快出"新专"的!
参考资料:
Go管道详解
https://www.jianshu.com/p/cb37d1701ca4