的使用go_Go完结篇Channel使用

f8e3962a0bddd5d630757e973ecf5db0.png

    这这这本来是要昨天发的,昨天回家光顾着收拾了b4ff89d008da31834a61b5c8d2cfb495.png所以,完结篇来的稍晚一步。

0ba53aee7757eee5b1711d5ff0242ea4.png

    这篇总结是紧接着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 可发送数据的类型。我们可以用一张图来简单描述一下。

8491ec8c0dca326c7664c07e7967c518.png

02

Channel的一些特性

  1. Channel的本质是一个队列

  2. Channel是线程安全的, 也就是自带锁定功能

  3. 遵循先入先出(First In First Out)的规则,保证收发数据的顺序

  4. 在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据

  5. 管道和切片/字典一样,必须创建后才能使用,否则会报错

  6. 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的遍历

 两种方式:

  1. for...range循环

  2. 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 ",     }}

输出结果:

2dd3a815656d8fbc9d473b523fc8c424.png

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天的学习对我来说更多的是一次知识回顾,从中确实有很多不一样的体会,接下来会进入更高阶的学习阶段。离新年还有一个月左右的时间,希望自己可以利用好这段时间继续加油。我会尽快出"新专"的!eeff6c3dc3cbdd1831f1e6066d51d1e3.png

参考资料:

Go管道详解

https://www.jianshu.com/p/cb37d1701ca4
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值