21·并发编程进阶

Channel 通道

channelchansendreceive

第 21 课:Channel 通道

学习目标

完成本课程后,你将能够:

  1. 理解 Channel 在 Go 并发中的核心作用——安全的通信。
  2. 掌握创建、发送(send)和接收(receive) Channel 数据的基本操作。
  3. 理解无缓冲(unbuffered)和有缓冲(buffered) Channel 的行为差异及其导致的阻塞特性。
  4. 了解双向和单向(方向限制) Channel 的使用场景。
  5. 能够编写使用 Channel 进行 goroutine 间通信的简单程序。

核心概念

1. 什么是 Channel?

想象一下两个人需要传递物品。他们可以通过一个管道(pipe)来传递,一人从一端放入,另一人从另一端取出。在 Go 中,Channel 就是这样的“管道”,用于在并发的 goroutine 之间传递数据。它是 Go 并发哲学“不要通过共享内存来通信,而要通过通信来共享内存”的核心体现。

Channel 是类型相关的,例如 chan int 表示一个只能传递 int 类型数据的 Channel。

2. 基本操作:发送与接收

  • 创建:使用 make 函数。ch := make(chan int) 创建一个无缓冲的 int 类型 Channel。
  • 发送:使用 <- 操作符,箭头方向指向 Channel。ch <- valuevalue 发送到 ch
  • 接收:同样使用 <- 操作符,箭头方向从 Channel 指出。value := <-chch 接收数据并赋值给 value

关键特性发送和接收操作都会导致 goroutine 阻塞,直到另一端准备好。

  • 对于无缓冲 Channel:发送方会阻塞,直到有接收方准备好接收;接收方会阻塞,直到有发送方发送数据。它强制进行同步,像两人直接握手交接物品。
  • 对于有缓冲 Channel:创建时指定容量,如 ch := make(chan int, 5)。发送方只在缓冲区满时阻塞;接收方只在缓冲区空时阻塞。它像一个有容量的队列。

3. 关闭 Channel

发送方可以使用 close(ch) 来关闭 Channel,表示不再有数据发送。接收方可以通过第二个返回值来判断 Channel 是否已关闭:

val, ok := <-ch // ok 为 false 时,表示 Channel 已关闭且没有值可接收

重要:永远不要关闭一个接收方的 Channel,通常由发送方在确认不再发送数据后关闭。

代码示例

示例1:基础的无缓冲 Channel 通信

package main

import (
	"fmt"
	"time"
)

// 生产者:向 channel 发送数据
func producer(ch chan<- int) {
	fmt.Println("生产者准备发送数据...")
	ch <- 100 // 发送数据,会阻塞,直到消费者接收
	fmt.Println("生产者发送完毕")
}

// 消费者:从 channel 接收数据
func consumer(ch <-chan int) {
	fmt.Println("消费者准备接收数据...")
	val := <-ch // 接收数据,会阻塞,直到生产者发送
	fmt.Println("消费者收到数据:", val)
}

func main() {
	// 创建一个无缓冲的 int channel
	ch := make(chan int)

	go producer(ch) // 启动生产者 goroutine
	go consumer(ch) // 启动消费者 goroutine

	// 等待一段时间,让 goroutine 有机会执行,实际应用中会用更优雅的方式同步
	time.Sleep(time.Second)
	fmt.Println("主函数结束")
}

示例2:有缓冲 Channel 示例

package main

import "fmt"

func main() {
	// 创建一个容量为 2 的缓冲 channel
	ch := make(chan string, 2)

	// 连续发送两次,不会立即阻塞(因为缓冲区有空间)
	ch <- "Hello"
	ch <- "Go"

	// 连续接收两次
	fmt.Println(<-ch) // 输出: Hello
	fmt.Println(<-ch) // 输出: Go

	// 如果尝试发送第三次,此时缓冲区已满,会导致死锁(主goroutine阻塞)
	// ch <- "World" // 取消注释将导致死锁
}

示例3:方向限制 Channel

package main

import "fmt"

// 此函数只能向 channel 发送数据
func sendOnly(ch chan<- int, val int) {
	ch <- val
}

// 此函数只能从 channel 接收数据
func receiveOnly(ch <-chan int) int {
	return <-ch
}

func main() {
	// 一个双向 channel
	biCh := make(chan int)

	// 将双向 channel 传递给需要方向限制的函数
	go sendOnly(biCh, 42)

	result := receiveOnly(biCh)
	fmt.Println("结果:", result) // 输出: 结果: 42
}

实践练习

练习1:基础数据交换

编写两个 goroutine,一个发送一个字符串 "ping" 到 Channel,另一个接收并打印它。确保程序不会因 goroutine 提前退出而结束。 预期输出ping

练习2:探索缓冲 Channel

  1. 创建一个容量为 3 的 chan bool
  2. 启动一个 goroutine 向其中发送 3 次 true,每次发送后打印 "Sent"
  3. 在主 goroutine 中,等待 1 秒后,连续接收 3 次并打印接收到的值。
  4. 思考:为什么所有 "Sent" 会在接收开始前打印出来? 预期输出(顺序):
Sent
Sent
Sent
true
true
true

练习3:生产者-消费者模型

模拟一个简单的生产者-消费者模型:

  1. 生产者 goroutine 生成数字 1 到 5,并发送到 Channel。
  2. 消费者 goroutine 从 Channel 接收这些数字并计算它们的总和。
  3. 主 goroutine 等待消费者完成,并输出总和。 提示:生产者发送完毕后需要关闭 Channel,消费者需要通过循环或 range 来接收所有数据。 预期输出总和: 15

常见错误

  1. 死锁(Deadlock):最常见的错误。当所有 goroutine 都在等待,没有一个能继续执行时发生。

    • 场景:只在一个 goroutine 中对一个无缓冲 Channel 进行发送或接收。
    • 示例ch := make(chan int); ch <- 1 // 主 goroutine 阻塞,没有其他 goroutine 来接收
    • 解决:确保有匹配的发送和接收方在并发运行,或使用有缓冲 Channel 并确保其容量足够。
  2. 忘记关闭 Channel:导致接收方的 range 循环永远阻塞。

    • 场景:使用 for val := range ch 循环接收数据,但发送方忘记在发送完所有数据后调用 close(ch)
    • 解决:在发送方确认不再发送后,显式关闭 Channel。
  3. 误解阻塞时机:认为有缓冲 Channel 永远不会阻塞。

    • 解决:牢记:缓冲满时发送阻塞,缓冲空时接收阻塞。
  4. 在接收方关闭 Channel:这可能导致“send on closed channel” panic。

    • 解决:明确所有权,通常由发送数据的一方负责关闭 Channel。
  5. 忽略接收零值:当 Channel 已关闭且没有值时,接收操作会立即返回该类型的零值(如 int0string"")。

    • 解决:始终使用 val, ok := <-ch 来检查是否成功接收。

小结

  • Channel 是 Go 中用于 goroutine 间通信的并发安全的数据管道。
  • 操作:使用 <- 进行发送(ch <- val)和接收(val <- ch)。创建用 make(chan Type)make(chan Type, Capacity)
  • 阻塞规则:这是 Channel 的核心行为。无缓冲 Channel 要求收发同步;有缓冲 Channel 在缓冲区满/空时阻塞。
  • 关闭:发送方使用 close(ch) 关闭 Channel,表明数据发送结束。接收方通过 val, ok := <-chok 值判断。
  • 方向限制chan<- 只发送,<-chan 只接收,用于函数参数类型声明,增强程序安全性和可读性。

理解并善用 Channel 的阻塞特性,是编写正确、高效并发程序的关键。在下一课中,我们将学习由这些基础特性衍生出的常见 Channel 模式。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「Channel 模式与实践」 以巩固所学知识。