介绍 Golang 通道(channel)

    技术2025-05-04  49

    介绍 Golang 通道(channel)

    本文介绍如何使用Golang通道。通道是Go应用中链接协程通信的管道,协程可以往通道中推入值或从中读取值。利用通道可以非常方便地实现高性能、高并发应用,相比与其他语言更简单,这并不是巧合,而是Go语言的设计理念————并发作为语言的一等公民,使得并发应用尽可能简单而不失灵活性。

    通道的思想起始很早就有了,但Go的实现者希望通道承担更多使命————以尽可能简单的方式让开发者创建更好、更清晰的高性能并发应用。确实,Go语言最吸引我的能力是可以轻松创建并发应用,到目前为止,不得不说这是一种乐趣。

    1. 从简单示例开始

    我们首先从简单示例开始。创建一个函数负责计算生成一个随机数并传给通道:

    package main import ( "fmt" "math/rand" ) func CalculateValue(values chan int) { // 设置随机种子,避免随机函数生成相同的值 rand.Seed(time.Now().UnixNano()) value := rand.Intn(10) fmt.Println("计算随机值: {}", value) // 往通道发送值 values <- value } func main() { fmt.Println("Golang Channel 教程") // 创建int类型通道,只能传入int类型值 values := make(chan int) defer close(values) go CalculateValue(values) // 从通道接收值 value := <-values fmt.Println(value) }

    我们分析下上面的程序。main函数中调用values := make(chan int) 新建通道变量values,后续作为参数传入CalculateValue函数。

    注:使用make实例化通道,和map、slice一样,channel使用前必须先用make实例化。

    创建通道之后,接着调用defer close(values) ,确保main函数完成之前调用关闭方法。这种方法通常被认为最佳实践,让代码更整洁。 接着启动协程CalculateValue(values),新建的通道作为函数参数。函数内部计算一个值(1~10),打印结果并发送给通道。

    回到main函数中,调用value := <-values从通道中接收值并打印。

    注:当我们执行该程序时,程序并没有立刻结束。这是因为给通道发送值或从通道接收值都会阻塞当前协程。main函数被阻塞直到从通道中接收到值为止。

    执行代码,查看运行结果:

    Go Channel Tutorial Calculated Random Value: {} 1 1

    我们看到实例化并使用通道非常直接、简单,下面继续看稍微复杂的场景。

    2. 无缓冲通道

    虽然通道非常方便,但有时在协程中使用通道会导致问题行为,与预期不一致。默认无缓存通道,当一个协程给其发送一个值,该协程会被阻塞,直到通道中的值被接收。 让我们看一个真实示例,代码与上节示例类似,但是我们扩展CalculateValue()函数在在发送值给通道之后增加一行输出语句。

    在main函数中增加一句go CalculateValue(valueChannel),我们期望两个值发往通道。

    package main import ( "fmt" "math/rand" "time" ) func CalculateValue(c chan int) { // 设置随机种子,避免随机函数生成相同的值 rand.Seed(time.Now().UnixNano()) value := rand.Intn(10) fmt.Println("计算随机值: {}", value) time.Sleep(1000 * time.Millisecond) c <- value fmt.Println("仅当另一个协程执行从通道中取值之后才执行") } func main() { fmt.Println("Golang Channel 教程") // 创建int类型通道,只能传入int类型值 valueChannel := make(chan int) defer close(valueChannel) go CalculateValue(valueChannel) go CalculateValue(valueChannel) // 从通道接收值 values := <-valueChannel fmt.Println(values) }

    执行程序,我们仅看到第一个协程执行了最后一行输出语句:

    Golang Channel 教程 计算随机值: {} 4 仅当另一个协程执行从通道中取值之后才执行 4

    这是在第二个协程中调用 c <- value语句阻塞了,后续main函数在第二个协程执行之前提前结束。

    3. 缓存通道

    绕过这种阻塞行为的方法是使用缓冲通道。缓存通道本质是使用给定大小的队列实现跨协程通信。创建缓存通道需要make函数中指定容量:

    bufferedChannel := make(chan int, 2)

    通过指定容量创建缓存通道,当协程再执行 c <- value时只有缓存通道满了才会阻塞。

    我们修改上面的程序使用缓存通道,同时在main函数中增加等待语句,确保main函数最后完成。

    func CalculateValue(c chan int) { // 设置随机种子,避免随机函数生成相同的值 rand.Seed(time.Now().UnixNano()) value := rand.Intn(10) fmt.Println("计算随机值: {}", value) time.Sleep(1000 * time.Millisecond) c <- value fmt.Println("仅当另一个协程执行从通道中取值之后才执行") } func main() { fmt.Println("Golang Channel 教程") // 创建int类型通道,只能传入int类型值 valueChannel := make(chan int, 2) defer close(valueChannel) go CalculateValue(valueChannel) go CalculateValue(valueChannel) // 从通道接收值 values := <-valueChannel fmt.Println(values) time.Sleep(1000 * time.Millisecond) }

    现在执行程序应该能够看到第二个协程继续执行,无论通道中的值是否被接收。通过对比我们看到两种通道的差异:非缓存通道默认阻塞,而缓存通道当没有满时不阻塞。 输出结果:

    Golang Channel 教程 计算随机值: {} 6 仅当另一个协程执行从通道中取值之后才执行 6 仅当另一个协程执行从通道中取值之后才执行

    4. 总结

    本文我们讨论了Golang的通道,了解两种通道的差异以及如何在Golang并发应用中使用。

    Processed: 0.011, SQL: 9