Go 开发常用操作技巧--channel

2023-01-09 19:21:03 浏览数 (2)

在 Go 语言中,提倡使用通信来共享内存,而不是通过共享内存来通信,这里的通信就是通过 channel 发送接收消息的方式进行数据传递,而不是通过修改同一个变量。所以在数据流动、传递的场景中要考虑优先使用 channel,它是并发安全的,性能也不错。

channel 声明

代码语言:javascript复制
ch := make(chan string)
  • 使用 make 函数
  • chan 是关键字,表示 channel 类型,chan 是一个集合类型
  • string 表示 channel 里存放数据的类型

在声明 channel 时,需要指定被共享的数据类型,其中可以通过 channel 共享的类型有 内置类型、命名类型、结构类型和引用类型的值或指针。

channel 使用

chan 只有发送和接收两种操作:

发送:<-chan //向chan内发送数据 接收:chan-> //从chan中获取数据

channel 分为有缓冲和无缓冲两种,下面来分别介绍下。

无缓冲 channel

无缓冲 channel,通道的容量是0,它不能存储数据,只是起到了传输的作用,所以无缓冲 channel 的发送和接收操作是同时进行的。

示例:

代码语言:javascript复制
package main

import (
 "fmt"
)

func main() {
 ch := make(chan string)
 go func(){
  fmt.Println("微客鸟窝")
  ch <- "执行完毕"
 }()

 fmt.Println("我是无尘啊")
 value := <-ch
 fmt.Println("获取的chan的值为:",value)
}

运行结果:

代码语言:javascript复制
我是无尘啊
微客鸟窝
获取的chan的值为: 执行完毕

有缓冲 channel

在声明的时候,我们可以传入第二个参数,即「channel容量大小」,这样就是创建了一个有缓冲 channel。

  • 有缓冲 channel 内部有一个队列
  • 发送操作是向队列尾部追加元素,如果队列满了,则阻塞等待,直到接收操作从队列中取走元素。
  • 接收操作是从队列头部取走元素,如果队列为空,则阻塞等待,直到发送操作向队列追加了元素。
  • 可以通过内置函数 cap() 来获取 channel 的容量,通过内置函数 len() 获取 channel 中元素个数。
代码语言:javascript复制
ch := make(chan int,3)
ch <- 1
ch <- 2
fmt.Println("容量为",cap(ch),"元素个数为:",len(ch))
//打印结果:容量为 3 元素个数为:2

关闭 channel

使用内置函数 close(ch)

对端可以判断channel是否关闭

代码语言:javascript复制
if num,ok := <- ch;ok == true{
 //如果对端已经关闭,ok-->false
 //如果对端没有关闭,ok-->true
}

也可以使用range 替代ok

代码语言:javascript复制
//当channel关闭时,range方式会将里面剩余的数据全部读取完成,再退出
//ch不能替换为<-ch ; 
for num := range ch{
  fmt.Println("读到数据:",num)
}

channel 关闭了就不能再向其发送数据了,否则会引起 panic 异常。 可以从关闭了的 channel 中接收数据,如果没数据,则接收到的是元素类型的零值。

单向 channel

只能发送或者只能接收的 channel 为单向 channel。

代码语言:javascript复制
send := make(ch<- int) //只能发送数据给channel
receive := make(<-ch int) //只能从channel中接收数据

示例:

代码语言:javascript复制
package main

import (
 "fmt"
)
//只能发送通道
func send(s chan<- string){
 s <- "微客鸟窝"
}
//只能接收通道
func receive(r <-chan string){
 str := <-r
 fmt.Println("str:",str)
}
func main() {
 //创建一个双向通道
 ch := make(chan string)
 go send(ch)
 receive(ch)
}

//运行结果: str: 微客鸟窝

实战技巧

控制 goroutine 并发的数量

代码语言:javascript复制
func main(){
 m1 := make(chan bool)
 go func() {
   for{
   select {
   case <-m1:
    fmt.Println("close")
    return
   case <-time.After(time.Millisecond*10000):
    fmt.Println("-----")
   }
  }
 }()

 // 控制 goroutine 并发的数量
 limit := make(chan bool,3)
 wg := sync.WaitGroup{}
 for i :=0;i<10;i  {
  limit <- true
  wg.Add(1)
  go func(i int) {
   defer func() {
    <- limit
    wg.Done()
   }()
   //HandleLogic()
   fmt.Println(i)
  }(i)
 }
 wg.Wait()

 m1 <- true
 close(m1)
}

for select 无限循环模式

一般是和 channel 组合完成任务,格式为:

代码语言:javascript复制
for { //for 无限循环,或者使用 for range 循环
  select {
    //通过 channel 控制
    case <-done:
      return
    default:
      //执行具体的任务
  }
}

这种是 for select 多路复用的并发模式,哪个 case 满足条件就执行对应的分支,直到有满足退出的条件,才会退出循环。没有退出条件满足时,则会一直执行 default 分支。

select timeout 模式

假如一个请求需要访问服务器获取数据,但是可能因为网络问题而迟迟获取不到响应,这时候就需要设置一个超时时间:

代码语言:javascript复制
package main

import (
 "fmt"
 "time"
)

func main() {
 result := make(chan string)
 timeout := time.After(3 * time.Second) //3秒后超时
 go func() {
  //模拟网络访问
  time.Sleep(5 * time.Second)
  result <- "服务端结果"
 }()
 for {
  select {
  case v := <-result:
   fmt.Println(v)
  case <-timeout:
   fmt.Println("网络访问超时了")
   return
  default:
   fmt.Println("等待...")
   time.Sleep(1 * time.Second)
  }
 }
}

运行结果:

代码语言:javascript复制
等待...
等待...
等待...
网络访问超时了

select timeout 模式核心是通过 time.After 函数设置的超时时间,防止因为异常造成 select 语句无限等待。 注意:不要写成这样 case <- time.After(time.Second), 这样是本次监听动作的超时时间,意思就说,只有在本次 select 操作中会有效,再次 select 又会重新开始计时,但是有default ,每次都会走到 default,那case 超时操作,肯定执行不到了。


图片及部分相关技术知识点来源于网络搜索,侵权删!

0 人点赞