前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go Channel(收藏以备面试)

Go Channel(收藏以备面试)

作者头像
一行舟
发布2022-08-25 13:59:40
4720
发布2022-08-25 13:59:40
举报
文章被收录于专栏:一行舟

Hi,我是行舟,今天和大家一起学习Go语言的Channel。

Go语言采用CSP模型,让两个独立执行的程序通过消息传递的方式共享内存,Channel就是Golang用来完成消息通讯的数据类型。

Go语言中,仍然可以使用共享内存的方式在多个协程间共享数据,只不过不推荐使用。

声明Channel

声明一个通道

代码语言:javascript
复制
var Channel类型 = chan 元素类型

除了上面的声明方式,还可以在chan的左右添加<-符号,分别表示只读通道和只写通道。 看几个实际的例子:

代码语言:javascript
复制
package  main

import "fmt"

func main()  {
   var c1 chan int        // 可读写的通道
   var c2 chan<- float64  // 只写通道
   var c3 <-chan int      // 只读通道

   fmt.Printf("c1=%+v \n",c1)
   fmt.Printf("c2=%+v \n",c2)
   fmt.Printf("c3=%+v \n",c3)
}

只声明未初始化的通道值是nil,需要初始化之后才会分配存储空间,通道初始化使用make方法。make方法的第二个参数定义了通道可以缓冲参数的个数。

代码语言:javascript
复制
c1 := make(chan int) // 初始化无缓冲的通道
c2 := make(chan float64,10) // 初始化可以缓冲10个元素的通道

fmt.Printf("c1=%+v \n",c1)
fmt.Printf("c2=%+v \n",c2)

如上面代码c1这种没有缓冲空间的通道,我们称为无缓冲通道;c2称为有缓冲通道。

基本用法

写入和读取数据

我们执行下面的代码

代码语言:javascript
复制
c1 := make(chan int , 10) // 初始化可以缓冲10个元素的通道
c1 <-1
c1 <-2 

初始化通道c1,并写入数据。

看下一个例子

代码语言:javascript
复制
c1 := make(chan int , 10) // 初始化可以缓冲10个元素的通道
c2 := make(chan float64) // 初始化无缓冲通道

c1 <- 1  // 往通道c1写值
c2 <- 1.01 // 往通道c2写值,会报错

此时将会看到这样的报错:

代码语言:javascript
复制
fatal error: all goroutines are asleep - deadlock!

这是因为我们往c2这个无缓冲通道中,写入数据,而c2没有读操作。我们加一行对c2的读操作

代码语言:javascript
复制
c2 := make(chan float64) // 初始化无缓冲通道

c1 <- 1  // 往通道c1写值
c2 <- 1.01 //往通道c2写值
<-c2

此时还是报和上面同样的错误。这是因为无缓冲通道的读写必须位于不同的协程中。

代码语言:javascript
复制
c1 := make(chan int , 10) // 初始化可以缓冲10个元素的通道
c2 := make(chan float64) // 初始化无缓冲通道

go func() {
   fmt.Printf("c2=%+v \n", <-c2) // 读取c2中的数据,输出c2=1.01
}()

c2 <- 1.01 // 往通道c2写值 
c1 <- 1  // 往通道c1写值

time.Sleep(1*time.Second) // 短暂的sleep,等待协程读取channel数据

这样写,才是正确的方式,程序可以正常运行。

读取通道的数据时,通道左边如果是一个变量,会返回通道中的元素;如果是两个变量,第一个是通道中复制出来的元素,第二个是通道的状态。其中通道的状态为true时,通道未关闭,状态为fasle时,通道关闭。已经关闭的通道不允许再发送数据。

代码语言:javascript
复制
c1 := make(chan int , 10) // 初始化可以缓冲10个元素的通道
c1 <- 1  // 往通道c1写值

ret,status := <- c1
fmt.Printf("r=%+v,status=%+v",ret,status) // 输出 r=1,status=true

关闭通道

关闭通道的方法是close方法。

代码语言:javascript
复制
c := make(chan int ,5)
c<-1
close(c)
c<-2 // 往关闭的通道中发送数据会报错

调用close方法关闭c通道,然后继续往c通道发送数据会报错。

代码语言:javascript
复制
panic: send on closed channel

调用close方法关闭通道时,会给所有等待读取通道数据的协程发送消息。这是一个非常有用的特性。

代码语言:javascript
复制
c := make(chan int)

go func() {
   ret,status := <-c
   fmt.Printf("go rountine 1 ret=%+v,status=%+v \n",ret,status)
}()

go func() {
   ret,status := <-c
   fmt.Printf("go rountine 2 ret=%+v,status=%+v \n",ret,status)
}()

close(c)  
time.Sleep(1*time.Second) 

虽然通道可以关闭,但并不是一个必须执行的方法,因为通道本身会通过垃圾回收器,根据它是否可以访问来决定是否回收。

遍历通道

遍历通道内的所有数据

代码语言:javascript
复制
c := make(chan int, 5)

c <- 1
c <- 2
c <- 3
c <- 4
c <- 5

go func() {
   for ret := range c{
      fmt.Printf("ret=%d \n",ret)
   }
}()

time.Sleep(2*time.Second)

上文很多例子中都在示例的最后加了 time.Sleep(1*time.Second) ,让主程序等待1s钟之后再退出。因为main函数也是一个goroutine,它执行完成就会退出,而不会判断是否有其他协程需要执行。我们让main goroutine等待1s钟,给其他协程足够的执行时间。

select

select是Golang中的控制结构,和其它语言的switch语句写法类似。不过select的case语句必须是通道的读写操作。

如果有多个 case 都可以运行,select 会随机选出一个执行,其他case不会执行。default在没有case可 执行时,总可以执行。

如下示例

代码语言:javascript
复制
c1 := make(chan int ,10)
c2 := make(chan int ,10)
c3 := make(chan int ,10)
var i1, i2 int

c1 <- 10
c3 <- 20
select {
   case i1 = <-c1:
      fmt.Printf("received i1=%d \n", i1)  // 输出 received i1=10
   case c2 <- i2:
      fmt.Printf("sent %d \n", i2 ) // 输出 sent 0 
   case i3, ok := (<-c3):  // 等价于 i3, ok := <-c3
      if ok {
         fmt.Printf("received i3=%d \n", i3) // 输出received i3=20
      } else {
         fmt.Printf("c3 is closed\n")
      }
   default:
      fmt.Printf("no communication\n")
}

我们多次运行这段代码会发现, 三个case都有可能执行到,这也验证了,select在满足多个case操作时,会在满足条件的case中随机选择一个执行。

当select语句没有case条件满足,且没有定义default语句时,当前select所在协程会陷入阻塞状态。

通过time.After( 1* time.Second),方法在1s之后会给通道发送消息,完成对select的超时操作:

代码语言:javascript
复制
c1 := make(chan int)

select{
   case <- c1:
      fmt.Println("print c1" )
   case <-time.After( 1* time.Second):
      fmt.Println("print 1s钟" )
}

select经常和for一起使用,下面是两者一起使用的一些例子:

代码语言:javascript
复制
c1 := make(chan int ,10)
// 把数组中的元素依次放入Channel
for _, str := rang []string{"a","b","c"} {
    select{
        case <- done:
            return
        case c1 <- str
    }
}
代码语言:javascript
复制
done := make(chan int)
// 无限循环,直到满足某个条件,操作done通道,完成循环
for{
    select{
        case <- done:
            return
        default:
            //进行某些操作
    }
}

通道的特性

通过学习Golang语言源码和一些教程中了解到,通道有几个重要的特性,需要理解并牢记。

  1. 通道可以作为参数在函数中传递,当作参数传递时,复制的是引用。
  2. 通道是并发安全的。
  3. 同一个通道的发送操作之间是互斥的,必须一个执行完了再执行下一个。接收操作和发送操作一样。
  4. 缓冲通道的发送操作需要复制元素值,然后在通道内存放一个副本。非缓冲通道则直接复制元素值的副本到接收操作。
  5. 往通道内复制的元素如果是引用类型,则复制的是引用类型的地址。
  6. 缓冲通道中的值放满之后,再往通道内发送数据,操作会阻塞。当有值被取走之后,会优先通知最早被阻塞的goroutine,重新发送数据。如果缓冲通道中的值为空,再从缓冲通道中接收数据也会被阻塞,当有新的值到来时,会优先通知最早被堵塞的goroutine,再次执行接收操作。
  7. 非缓冲通道,无论读写,都是堵塞的,都需要找到配对的操作方才能执行。
  8. 对于刚初始化的nil通道,他的发送和接收操作会永远阻塞。

高级示例

我们使用Channel完成两个常见的问题,以加深对Channel的理解。第一个,借助通道,使两个协程交替输出大小写字母。

代码语言:javascript
复制
package main

import (
   "fmt"
   "time"
)

func main()  {
   arr1 := []string{"a","b","c","d","e"}
   arr2 := []string{"A","B","C","D","E"}
   a := make(chan bool)
   b := make(chan bool)

   go func() {
      for _,str := range arr1{
         if <-a {
            fmt.Printf(str)
            b <- true
         }
      }
   }()

   go func() {
      for _,str := range arr2{
         if <-b {
            fmt.Printf(str)
            a <- true
         }
      }
   }()

   a<-true

   time.Sleep(2*time.Second)
}

我们定义了a,b两个channel,利用无缓冲通道接收堵塞的特性,在两个goroutine中,接收通道的值并作为继续执行的依据,从而达到交替执行的目的。

第二个,爬取指定的网站

代码语言:javascript
复制
package main

import (
   "fmt"
   "time"
)

// 抓取网页内容
func crawl(url string) (result string) {
   time.Sleep(1*time.Second) // 睡眠1s钟模拟抓取完数据
   return url+":抓取内容完成 \n"
}

// 保存文件内容到本地
func saveFile(url string,limiter chan bool, exit chan bool) {
   fmt.Printf("开启一个抓取协程 \n")

   result := crawl(url)   // 抓取网页内容
   if result != "" {
      fmt.Printf(result)
   }
   <-limiter  // 通知限速协程,抓取完成
   if (exit != nil){
      exit<-true // 通知退出协程,程序执行完成
   }
}

// urls是要爬取的地址,n并发goroutine限制
func doWork(urls []string,n int) {
   limiter := make(chan bool,n) // 限速协程
   exit := make(chan bool) // 退出协程
   for i,value := range urls{
      limiter <- true
      if i == len(urls)-1 {
         go saveFile(value,limiter,exit)
      }else{
         go saveFile(value,limiter,nil)
      }
   }
   <-exit
}

func main() {
   urls := []string{"https://www.lixiang.com/","https://www.so.com","https://www.baidu.com/","https://www.360.com/"}
   doWork(urls, 1)
}

我们通过limiter协程的缓冲区大小,控制协程并发数量。通过exit协程的阻塞,结束最终程序。

实现原理

Channel在Golang中用hchan结构体表示。

代码语言:javascript
复制
type hchan struct {
   qcount   uint           // channel通道中元素个数
   dataqsiz uint           // 环形队列中数据大小
   buf      unsafe.Pointer // 存放实际元素的位置
   elemsize uint16  // channnel类型大小
   closed   uint32  // channnel是否关闭
   elemtype *_type // channel中元素类型
   sendx    uint   // 发送的goroutine在buf中的位置
   recvx    uint   // 接收的goroutine在buf中的位置
   recvq    waitq  // 等待读取的goroutine队列
   sendq    waitq  // 等待写入的goroutine队列

   // lock protects all fields in hchan, as well as several
   // fields in sudogs blocked on this channel.
   //
   // Do not change another G's status while holding this lock
   // (in particular, do not ready a G), as this can deadlock
   // with stack shrinking.
   lock mutex // channel并发锁
}

buf中存放了所有缓冲的数据,结合sendx,recvx,构造了一个环形队列结构。

通道初始化时,根据元素大小、是否含有指针决定存储空间的分配。当元素大小为0时,只分配hchan结构体的内存就可以了。当没有指针时,连续分配元素大小和结构体大小的内存。当存在指针时,需要给指针元素单独分配内存空间。

通道写入数据的过程中,首先判断是否有正在等待读取的协程,如果有的话,复制数据给此协程;否则继续判断是否有空闲缓冲区,如果有的话把数据复制到缓冲区;否则,把当前goroutine放入等待写入队列。

通道读取数据的流程和写入类似,首先判断是否有等待写入的协程,如果有的话,启动协程的写入操作,复制数据;否则继续判断缓冲区中是否有数据,如果有的话复制数据;否则,把当前goroutine放入等待读取的队列

Go Channel的源码,主要在runtime/chan.go目录下。

总结

本文主要介绍了Go Channel的基本用法,特性,常用场景和实现原理。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-09-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一行舟 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 声明Channel
  • 基本用法
    • 写入和读取数据
      • 关闭通道
        • 遍历通道
          • select
          • 通道的特性
          • 高级示例
          • 实现原理
          • 总结
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档