《10节课学会Golang-10-Channel》

微信关注【面试情报局】我们一起干翻面试官, 回复golang获取本文源码
视频地址:b站 10节课学会Golang,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:%d\n", 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 <- 1

最后,为了保证 Goroutine 有足够的时间去接收 Channel 中的值,通过 <-time.After(time.Second * 3) 等待 3 秒钟之后,打印出 “return”。如果将 <-time.After(time.Second * 3) 去掉,那么程序可能在打印 “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: %s\n", num)
}

上面代码创建了一个无缓冲的字符串类型的 Channel c,然后启动了一个新的 Goroutine,该 Goroutine 会在 10 秒后发送一个字符串 "ping"Channel c 中。在主 main 中,接收操作 <-c 会阻塞,直到有值从 Channel c 中被接收到为止。因为发送端需要 10 秒后才会发送数据,所以接收端会在 <-c 处阻塞 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: %d\n", i)
	}
	close(c)
}

func consumer(c chan int) {
	for {
		num, ok := <-c
		if !ok {
			fmt.Println("consumer closed")
			return
		}
		fmt.Printf("consumer received: %d\n", 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 <- true // panic: send on closed channel
}
操作一个零值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 := <- c 表达式接收第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么 v 为对应类型零值,ok 为 false
	v, ok := <-c
	fmt.Printf("value:%d, ok:%t\n", 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 := <- c 表达式第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完
	v, ok := <-c
	fmt.Printf("value:%d, ok:%t\n", v, ok)

	fmt.Println("close")
}

思考题

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

参考


系列文章

《10节课学会Golang-01-Package》
《10节课学会Golang-02-变量与常量》
《10节课学会Golang-03-函数》
《10节课学会Golang-04-流程控制》
《10节课学会Golang-05-结构体》
《10节课学会Golang-06-数组与切片》
《10节课学会Golang-07-Map》
《10节课学会Golang-08-Interface》
《10节课学会Golang-09-Goroutine》
《10节课学会Golang-10-Channel》

微信关注【面试情报局】我们一起干翻面试官, 回复golang获取本文源码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HTML网页设计-期末大作业

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值