《10节课学会Go-10-Channel》【】

本文是全系列中第10 / 10篇:10节课学会Go

Channel

Channel 是 Go 语言中一种用于在 Goroutine 之间传递数据的机制。Channel 通过通信实现共享内存,可以安全地传递数据,避免了多个 Goroutine 访问共享内存时出现的竞争和死锁问题。

Channel 可以是有缓冲或无缓冲的。无缓冲的 Channel,也称为同步 Channel,发送操作和接收操作必须同时准备就绪,否则会被阻塞。有缓冲的 Channel,也称为异步 Channel,发送操作会在 Channel 缓冲区未满的情况下立即返回,接收操作也会在 Channel 缓冲区不为空的情况下立即返回,否则会被阻塞。

定义Channel

package main

import (
  "fmt"
  "time"
)

// 定义 channel, channel 是带有类型的管道,可以通过信道操作符 func main() {
  // 信道在使用前必须通过内建函数 make 来创建

  // make(chan T,size)  标识用内建函数 make 来创建 一个T类型的缓冲大小为 size 的 channel
  // 如下: make(chan int) 用内建函数 make 来创建 一个 int 类型的缓冲大小为 0 的 channel
  c := make(chan int)

  go func() {
    // 从 c 接收值并赋予 num
    num := c
    fmt.Printf("recover:%dn", num)
  }()

  // 将 1 发送至信道 c
  c  1

  time.After(time.Second * 3)

  fmt.Println("return")
}

首先通过 make 函数创建了一个无缓冲的 int 类型的 Channel c,即:c := make(chan int)

然后通过 go 关键字定义了一个匿名的 Goroutine,用于从 Channel c 中接收数据。匿名 Goroutine 中,使用 语法从 Channel c 中接收值,并将其赋值给变量 num。接收完值后,使用 fmt.Printf 打印出接收到的值。

接着,在 main函数 中,使用 语法将整数值 1 发送到 Channel c 中,即:c 。

最后,为了保证 Goroutine 有足够的时间去接收 Channel 中的值,通过 等待 3 秒钟之后,打印出 "return"。如果将 去掉,那么程序可能在打印 "return" 之前就结束了,因为 Goroutine 没有足够的时间去接收 Channel 中的值。

无缓冲Channel

无缓冲的 Channel通过定义:

make(chan T)

在无缓冲的 Channel 中,发送和接收操作是同步的。如果一个 Goroutine 向一个无缓冲的 Channel 发送数据,它将一直阻塞,直到另一个 Goroutine 从该 Channel 中接收到数据。同样地,如果一个 Goroutine 从一个无缓冲的 Channel 中接收数据,它将一直阻塞,直到另一个 Goroutine 向该 Channel 中发送数据。

package main

import (
  "fmt"
  "time"
)

// 发送端和接收端的阻塞问题
// 发送端在没有准备好之前会阻塞,同样接收端在发送端没有准备好之前会阻塞
func main() {
  c := make(chan string)

  go func() {
    time.After(time.Second * 10)
    fmt.Println("发送端准备好了 send: ping")
    c  "ping" // 发送
  }()

  // 发送端10s后才准备好,所以阻塞在当前位置
  fmt.Println("阻塞在当前位置,发送端发送数据后才继续执行")
  num := c
  fmt.Printf("recover: %sn", num)
}

上面代码创建了一个无缓冲的字符串类型的 Channel c,然后启动了一个新的 Goroutine,该 Goroutine 会在 10 秒后发送一个字符串 "ping"Channel c 中。在主 main 中,接收操作 会阻塞,直到有值从 Channel c 中被接收到为止。因为发送端需要 10 秒后才会发送数据,所以接收端会在 处阻塞 10 秒。接收到 "ping" 后,主 main 继续执行,输出 "recover: ping"

小练习:通过goroutine+channel计算数组之和。

package main

import "fmt"

// 对切片中的数进行求和,将任务分配给两个 Go 程。一旦两个 Go 程完成了它们的计算,它就能算出最终的结果。

// sum 求和函数
func sum(s []int, c chan int) {
  ans := 0
  for _, v := range s {
    ans += v
  }
  c  ans // 将和送入 c
}

func main() {
  s := []int{1, 1, 1, 1, 1, 2, 2, 2, 2, 2}

  c := make(chan int)
  go sum(s[:len(s)/2], c)
  go sum(s[len(s)/2:], c)
  x, y := c, c // 从 c 中接收

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

缓冲Channel

缓冲channel定义:

make(chan T,size)

缓冲 Channel 是带有缓冲区的 Channel,创建时需要指定缓冲区大小,例如 make(chan int, 10) 创建了一个缓冲区大小为 10 的整型 Channel。

缓冲 Channel 中, 当缓冲区未满时,发送操作是非阻塞的,如果缓冲区已满,则发送操作会阻塞,直到有一个接收操作接收了一个值, 才能继续发送。当缓冲区非空时,接收操作是非阻塞的,如果缓冲区为空,则接收操作会阻塞,直到有一个发送操作发送了一个值。

package main

import (
  "fmt"
  "time"
)

func producer(c chan int, n int) {
  for i := 0; i  n; i++ {
    c  i
    fmt.Printf("producer sent: %dn", i)
  }
  close(c)
}

func consumer(c chan int) {
  for {
    num, ok := c
    if !ok {
      fmt.Println("consumer closed")
      return
    }
    fmt.Printf("consumer received: %dn", num)
  }
}

func main() {
  c := make(chan int, 5)
  go producer(c, 10)
  go consumer(c)
  time.Sleep(time.Second * 1)
  fmt.Println("main exited")
}

在上面代码中,我们创建了一个缓冲区大小为 5 的整型 Channel,生产者向 Channel 中发送了 10 个整数,消费者从 Channel 中接收这些整数,并将它们打印出来。由于缓冲区大小为 5,因此生产者只有在 Channel 中有 5 个或更少的元素时才会被阻塞。在该示例中,由于消费者从 Channel 中接收元素的速度比生产者发送元素的速度快,因此生产者最终会被阻塞,直到消费者接收完所有的元素并关闭 Channel。

需要注意的是,当 Channel 被关闭后,仍然可以从 Channel 中接收剩余的元素,但不能再向 Channel 中发送任何元素。因此,在消费者函数中,我们使用了 for 循环和 ok 标志来检查 Channel 是否已经被关闭。

非缓冲channel和缓冲channel的对比:

package main

import "fmt"

// 不带缓冲的 channel
func NoBufferChan() {
  ch := make(chan int)
  ch  1 // 被阻塞,执行报错 fatal error: all goroutines are asleep - deadlock!
  fmt.Println(ch)
}

// 带缓冲的 channel
func BufferChan() {
  // channel 有缓冲、是非阻塞的,直到写满 cap 个元素后才阻塞
  ch := make(chan int, 1)
  ch  1
  fmt.Println(ch)
}

func main() {
  //NoBufferChan()
  BufferChan()
}

关闭channel

Close 函数可以用于关闭 Channel,关闭一个channel后,可以从中读取数据不过读取的数据全是当前channel类型的零值,但不能向这个channel写入数据会发送panic。

package main

func main() {
  ch := make(chan bool)
  close(ch)
  fmt.Println( ch)
  //ch }
操作 一个零值nil通道 一个非零值但已关闭的通道 一个非零值且尚未关闭的通道
关闭 产生恐慌 产生恐慌 成功关闭
发送数据 永久阻塞 产生恐慌 阻塞或者成功发送
接收数据 永久阻塞 永不阻塞 阻塞或者成功接收

遍历 Channel

可以通过range持续读取channel,直到channel关闭。

package main

import (
    "fmt"
    "time"
)

// 通过 range 遍历 channel, 并通过关闭 channel 来退出循环

// 复制一个 channel 或用于函数参数传递时, 只是拷贝了一个 channel 的引用, 因此调用者和被调用者将引用同一个channel对象
func genNum(c chan int) {
    for i := 0; i  10; i++ {
        c  i
        time.Sleep(1 * time.Second)
    }
    // 发送者可通过 close 关闭一个信道来表示没有需要发送的值了
 close(c)
}

func main() {
    c := make(chan int, 10)
    go genNum(c)

    // 循环 for v := range c 会不断从信道接收值,直到它被关闭
 // 并且只有发送者才能关闭信道,而接收者不能, 向一个已经关闭的信道发送数据会引发程序恐慌(panic)
 for v := range c {
        fmt.Println("receive:", v)
    }

    // 接收者可以通过 v,ok :=     v, ok := c
    fmt.Printf("value:%d, ok:%tn", v, ok)

    fmt.Println("close")
}

通过select操作channel

通过select-case可以选择一个准备好数据channel执行,会从这个channel中读取或写入数据。

package main

import (
    "fmt"
    "time"
)

// 通过 channel+select 控制 goroutine 退出
func genNum(c, quit chan int) {
    for i := 0; ; i++ {
        // select 可以等待多个通信操作
     // select 会阻塞等待可执行分支。当多个分支都准备好时会随机选择一个执行。
     select {
        case quit:
            // 发送者可通过 close 关闭一个信道来表示没有需要发送的值了。
         close(c)
            return
        default: // 等同于 switch 的 default。当所以case都阻塞时如果有default则,执行default
         c  i
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go genNum(c, quit)

    // 循环 for v := range c 会不断从信道接收值,直到它被关闭
 // 并且只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。
 for i := 0; i  10; i++ {
        fmt.Println("receive:", c)
    }

    // 通知 genNum() 退出
 quit  1

    // 接收者可以通过 v,ok :=     v, ok := c
    fmt.Printf("value:%d, ok:%tn", v, ok)

    fmt.Println("close")
}

思考题

  1. 通过goroutine+channel统计文本文件中每个单词的数量。

微信搜一搜「面试情报局」第一时间阅读,回复【面霸】获取面试资料和简历模板


微信关注【面试情报局】我们一起干翻面试官。\ 项目地址 \

本文是全系列中第10 / 10篇:10节课学会Go
打赏 赞(0) 分享'
分享到...
微信
支付宝
微信二维码图片

微信扫描二维码打赏

支付宝二维码图片

支付宝扫描二维码打赏

文章目录