Go中的channel一般是怎么用的

Go语言中可以通过go关键字来开启一个goroutine,实现很简单,但是开启完goroutine之后都是各个goroutine各自处理自己的逻辑,但有时候我们需要不同的goroutine之间能够通信,这里就要用到channel

1. channel是什么

官方定义:
channels are a typed conduit through which you can send and receive values with the channel operator
其实简单来说channel就是一个可以收发数据的管道

2. channel初始化

channel的声明方式如下:

var channel_name chan channel_type
var channel_name [size]chan channel_type  // 声明一个channel,其容量大小为size

声明之后的管道,并没有进行初始化为其分配空间,其值是nil,我们要使用还要配合make函数来对其初始化,之后才可以在程序中使用该管道

channel_name = make(chan channel_type)
channel_name = make(chan channel_type, size)

或者我们可以直接一步完成

channel_name := make(chan channel_type)
channel_name := make(chan channel_type, size)  //创建带有缓存的管道,size为缓存大小

3. channel操作

3.1 传递数据

ch := make(chan int)         // 创建一个管道ch
ch <- v                      // 向管道ch中发送数据v.
v := <-ch                    // 从管道中读取数据存储到变量v
close(ch)                    // 关闭管道ch

在这里需要注意close(ch)这个操作,管道用完了,需要对其进行关闭,避免程序一直在等待以及资源的浪费。但是关闭的管道,仍然可以从中接收数据,只是接收到的的数据永远是零值
看下面例子:

package main

import (
   "fmt"
   "time"
)

func main() {
   ch := make(chan int, 5)
   ch <- 1
   close(ch)
   go func() {
      for i := 0; i < 5; i++ {
         v := <-ch
         fmt.Printf("v=%d\n", v)
      }
   }()
   time.Sleep(2 * time.Second)
}

运行结果:

v=1
v=0
v=0
v=0
v=0

创建一个缓存为5int类型的管道,向管道里写入一个1之后,将管道关闭,然后开启一个gortoutine从管道读取数据,读取5次,可以看到即便管道关闭之后,他仍然可以读取数据,在读完数据之后,将一直读取零值
但是,上述读取方式还有一个问题?比如我们创建一个int类型的channel,我们需要往里面写入零值,用另一个goroutine读取,此时我们就无法区分读取到的是正确的零值还是数据已经读去完了而读取到的零值 所以我们一般用以下两种常用的读取方式:

  • 判定读取

  • for range读取

3.1.1 判定读取

还是以上面的例子来看,稍作修改

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int, 5)
    ch <- 1
    close(ch)
    go func() {
       for i := 0; i < 5; i++ {
          v, ok := <-ch // 判断句式读取
          if ok {
             fmt.Printf("v=%d\n", v)
          } else {
             fmt.Printf("channel数据已读完,v=%d\n", v)
          }
       }
    }()
    time.Sleep(2 * time.Second)
}

运行结果:

v=1
channel数据已读完,v=0
channel数据已读完,v=0
channel数据已读完,v=0
channel数据已读完,v=0

在读取channel数据的时候,用ok句式做了判断,当管道内还有数据能读取的时候,oktrue,当管道关闭后,okfalse

3.1.2 for range读取

在上面例子中,我们明确了读取的次数是5次,但是我们往往在更多的时候,是不明确读取次数的,只是在channel的一段读取数据,有数据我们就读,直到另一段关闭了这个channel,这样就可以用for range这种优雅的方式来读取channel中的数据了

package main

import (
   "fmt"
   "time"
)

func main() {
   ch := make(chan int, 5)
   ch <- 1
   ch <- 2
   close(ch)
   go func() {
      for v := range ch {
         fmt.Printf("v=%d\n", v)
      }
   }()
   time.Sleep(2 * time.Second)
}

运行结果:

v=1
v=2

goroutinechannel里写了两个数据12,然后关闭,子channel也只能读取到12。这里在主goroutine关闭了channel之后,子goroutine里的for range循环才会结束

4. 双向channel和单向channel

channel根据其功能又可以分为双向channel和单向channel,双向channel即可发送数据又可接收数据,单向channel要么只能发送数据,要么只能接收数据
定义单向读channel

var ch = make(chan int)
type RChannel= <-chan int    // 定义类型
var rec RChannel = ch

定义单向写channel

var ch = make(chan int)
type SChannel = chan<- int  // 定义类型
var send SChannel = ch

注意写channel与读channel在定义的时候只是<-的位置不同,前者在chan关键字后面,后者在chan关键字前面
代码示例:

import (
    "fmt"
    "time"
)

type SChannel = chan<- int
type RChannel = <-chan int

func main() {
    var ch = make(chan int)  //  创建channel

    go func() {
        var send SChannel = ch
        fmt.Println("send: 100")
        send <- 100
    }()

    go func() {
        var rec RChannel = ch
        num := <- rec 
        fmt.Printf("receive: %d", num)
    }()
    time.Sleep(2*time.Second)
}

运行结果:

send: 100
receive: 100

创建一个channel ch,分别定义两个单向channel类型SChannelRChannel ,根据别名类型给ch定义两个别名sendrec,一个只用于发送,一个只用于读取

5. channel可以解决什么问题

channel非常重要,Go语言中有个重要思想: 不以共享内存来通信,而以通信来共享内存。 说得更直接点,协程之间可以利用channel来传递数据,如下的例子,可以看出父子协程如何通信的,父协程通过channel拿到了子协程执行的结果。

package main

import (
        "fmt"
        "time"
)

func sum(s []int, c chan int) {
        sum := 0
        for _, v := range s {
                sum += v
        }
        c <- sum // send sum to c
}

func main() {
        s := []int{7, 2, 8, -9, 4, 0}

        c := make(chan int)
        go func() {
                sum(s[:len(s)/2], c)
                //time.Sleep(1 * time.Second)
        }()
        go sum(s[len(s)/2:], c)
        x, y := <-c, <-c // receive from c

        fmt.Println(x, y, x+y)
}

运行结果:

-5 17 12

channel又分为两类:有缓冲 channel 和无缓冲 channel
这个在前面的代码示例中也有简单的描述了。为了协程安全,无论是有无缓冲的 channel,内部都会有一把锁来控制并发访问 同时 channel 底层一定有一个队列,来存储数据。 无缓冲 channel 可以理解为同步模式,即写入一个,如果没有消费者在消费,写入就会阻塞 有缓冲 channel 可以理解为异步模式。即写入消息之后,即使还没被消费,只要队列没满,就可继续写入。如图所示:

channel1

这里可能会问,如果有缓冲 channel 队列满了,那不就退化到同步了么?是的,如果队列满了,发送还是会阻塞

channel2

但是我们来反向思考下,如果有缓冲 channel 长期都处于满队列情况,那何必用有缓冲。所以预期在正常情况下,有缓冲 channel 都是异步交互的

5.1 扩展

上面说了当缓冲队列满了以后,继续往channel里面写数据,就会阻塞,那么利用这个特性,我们可以实现一个goroutine之间的锁
直接看示例:

package main

import (
   "fmt"
   "time"
)

func add(ch chan bool, num *int) {
   ch <- true
   *num = *num + 1
   <-ch
}

func main() {
   // 创建一个size为1的channel
   ch := make(chan bool, 1)

   var num int
   for i := 0; i < 100; i++ {
      go add(ch, &num)
   }

   time.Sleep(2)
   fmt.Println("num 的值:", num)
}

运行结果:

num 的值: 100

ch <- true<- ch就相当于一个锁,将 *num = *num + 1这个操作锁住了。因为ch管道的容量是1,在每个add函数里都会往channel放置一个true,直到执行完+1操作之后才将channel里的true取出。由于channelsize1,所以当一个goroutine在执行add函数的时候,其他goroutine执行add函数,执行到ch <- true的时候就会阻塞,*num = *num + 1不会成功,直到前一个+1操作完成,<-ch,读出了管道的元素,这样就实现了并发安全

6. 小结

  • 关闭一个未初始化的 channel 会产生 panic

  • channel只能被关闭一次,对同一个channel重复关闭会产生 panic

  • 向一个已关闭的 channel 发送消息会产生 panic

  • 从一个已关闭的channel读取消息不会发生panic,会一直读取所有数据,直到零值

  • channel可以读端和写端都可有多个goroutine操作,在一端关闭channel的时候,该channel读端的所有goroutine 都会收到channel已关闭的消息

  • channel是并发安全的,多个goroutine同时读取channel中的数据,不会产生并发安全问题

交流学习

如果您觉得文章有帮助,请帮忙转发给更多好友,或关注公众号:IT杨秀才,持续更新更多硬核文章,一起聊聊互联网网那些事儿!

公众号二维码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值