【Go 】Go 语言中的 channel介绍

Go 语言中的 channel 是基于 CSP(Communicating Sequential Processes)思想实现的通信机制。其核心理念是:

  • 通过通信共享内存,而不是通过共享内存通信

    也就是说,应当让不同 goroutine 之间通过传递数据来共享状态,避免直接共享内存而产生竞态问题。

  • FIFO 队列

    Channel 本身就是一个队列,按照先进先出的原则传递数据。

  • 阻塞与死锁

    • 无缓冲通道:发送操作和接收操作必须同时发生,否则将阻塞,容易发生死锁。
    • 有缓冲通道:有固定的缓冲区,当缓冲区满时发送操作阻塞;当缓冲区空时接收操作阻塞。
  • 数据类型限制

    每个 channel 只能存放同一种数据类型,且 channel 是引用类型,需要通过 make 进行初始化。

  • 同步通信与协程控制

    Channel 不仅用于数据传递,还可以用来实现不同 goroutine 之间的同步控制,比如等待子协程执行完毕再退出主协程。

  • 多路复用

    使用 select 语句可以同时监听多个 channel 的操作,实现非阻塞通信和超时控制。

1. 无缓冲通道的基本使用

知识点说明:

  • 无缓冲通道没有存储空间,发送和接收必须成对出现。当一方操作时,另一方必须就绪,否则就会阻塞。
  • 该示例展示了如何在一个 goroutine 中发送数据,通过主 goroutine 接收数据。

代码示例:

go 复制代码
// 文件名: 03channel_unbuffered.go
package main

import (
	"fmt"
	"time"
)

// 发送数据到无缓冲通道
func senddata(ch chan int) {
	ch <- 1
	fmt.Println("往通道里存放数据 1")
	ch <- 2
	fmt.Println("往通道里存放数据 2")
}

func main() {
	// 创建无缓冲通道(容量为0)
	ch1 := make(chan int)
	
	// 启动一个 goroutine 发送数据到通道
	go senddata(ch1)
	
	// 主 goroutine 休眠一段时间以等待子协程发送数据
	time.Sleep(3 * time.Second)
	
	// 按照先进先出的顺序接收数据
	data := <-ch1
	fmt.Println("拿到通道数据:", data)
	data2 := <-ch1
	fmt.Println("拿到通道数据:", data2)
	
	// 若再接收数据,由于通道中没有数据,将会阻塞
	// data3 := <-ch1
	// fmt.Println("第三次拿到通道数据:", data3)
}

2. 利用无缓冲通道实现协程同步

知识点说明:

  • 协程同步:在 Go 中,主协程退出会导致整个程序结束。如果想等待子协程执行完毕,可以利用无缓冲通道作为信号量。
  • 使用空结构体(struct{})作为信号传递,因为它不占用内存空间。

代码示例:

go 复制代码
// 文件名: 03channel_sync.go
package main

import (
	"fmt"
	"time"
)

// 定义空结构体,作为信号传递
type none struct{}

// 发送信号到无缓冲通道,模拟子协程任务完成
func senddata(ch chan none) {
	fmt.Println("this is senddata")
	time.Sleep(2 * time.Second)
	ch <- none{} // 发送一个空结构体信号
}

func main() {
	fmt.Println("start....")
	// 创建无缓冲通道(类型为 none)
	ch1 := make(chan none)
	
	// 启动子协程执行任务
	go senddata(ch1)
	
	// 主协程等待接收到信号后再继续
	<-ch1
	fmt.Println("end....")
}

3. 通道的关闭与数据消费

知识点说明:

  • 通道关闭 :发送方完成数据发送后,通过 close(ch) 关闭通道,可以防止消费者无限等待数据。
  • 数据消费 :接收方可以通过三种方式判断通道是否关闭:
    1. 判断读取到的值是否为数据类型的零值;
    2. 使用 value, ok := <-ch 判断;
    3. 使用 range 自动遍历,直到通道关闭。

代码示例:

go 复制代码
// 文件名: 03channel_close.go
package main

import (
	"fmt"
)

// 发送数据并关闭通道
func sendata(ch chan string) {
	for i := 0; i < 3; i++ {
		ch <- fmt.Sprintf("发送数据%d", i)
	}
	// 打印提示并关闭通道,确保接收方不会因无数据而死锁
	defer fmt.Println("发送数据完毕")
	defer close(ch)
}

func main() {
	ch1 := make(chan string)
	go sendata(ch1)

	// 方式1:读取数据后判断零值(适用于数据类型零值不会作为有效数据的情况)
	/*
	for {
		data := <-ch1
		if data == "" { // 字符串的零值为 ""
			break
		}
		fmt.Println("从通道中获取到的数据:", data)
	}
	*/

	// 方式2:使用 value, ok 判断通道是否关闭
	/*
	for {
		data, ok := <-ch1
		if !ok {
			break
		}
		fmt.Println("2从通道中获取到的数据:", data)
	}
	*/

	// 方式3:使用 range 遍历通道(推荐方式)
	for value := range ch1 {
		fmt.Println("3从通道中获取到的数据:", value)
	}
}

4. 有缓冲通道的使用

知识点说明:

  • 有缓冲通道:在创建通道时指定缓冲区大小,允许发送者先存储一定数量的数据而不必等待接收者。
  • 当缓冲区满时,发送操作将阻塞;当缓冲区空时,接收操作将阻塞。
  • 适用于生产者和消费者速度不一致的场景。

代码示例:

go 复制代码
// 文件名: 03channel_buffered.go
package main

import (
	"fmt"
	"time"
)

// 向有缓冲通道发送数据
func senddata(ch chan string) {
	for i := 0; i <= 10; i++ {
		ch <- fmt.Sprintf("放入数据%d", i)
		fmt.Printf("往通道放入数据%d\n", i)
	}
	// 发送完成后关闭通道
	defer close(ch)
}

func main() {
	// 创建一个带缓冲的通道,缓冲区大小为 6
	ch1 := make(chan string, 6)
	go senddata(ch1)
	
	// 等待一定时间,保证数据发送到缓冲区
	time.Sleep(2 * time.Second)
	
	// 使用 range 循环读取数据,直到通道关闭
	for v := range ch1 {
		fmt.Println("读取通道数据:", v)
	}
}

5. 使用 select 监听多个通道

知识点说明:

  • select 语句:允许同时监听多个通道操作,当其中一个通道操作准备就绪时执行对应的 case;若多个同时就绪,则随机选择。
  • 超时控制 :结合 time.After 实现超时等待,避免长时间阻塞。
  • default 分支:用于非阻塞检测,如果没有通道操作准备好,则执行 default 分支。

代码示例:

go 复制代码
// 文件名: 03channel_select.go
package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	ch3 := make(chan int)

	// 分别启动三个 goroutine,在不同时间发送数据到各自的通道
	go func() {
		time.Sleep(10 * time.Second)
		ch1 <- 1
	}()
	go func() {
		time.Sleep(2 * time.Second)
		ch2 <- 2
	}()
	go func() {
		time.Sleep(3 * time.Second)
		ch3 <- 3
	}()

	// 循环监听 3 次 select 中的不同通道或超时/default 分支
	for i := 0; i < 3; i++ {
		select {
		case msg := <-ch1:
			fmt.Println("接收到数据 from ch1:", msg)
		case msg := <-ch2:
			fmt.Println("接收到数据 from ch2:", msg)
		case msg := <-ch3:
			fmt.Println("接收到数据 from ch3:", msg)
		// 超时控制
		case <-time.After(1 * time.Second):
			fmt.Println("设置超时时间!timeout")
		// 非阻塞操作
		default:
			fmt.Println("没有消息准备好")
		}
	}
}

总结

本次整理中,我们按照以下知识点对 channel 的使用进行了归类整理:

  1. 无缓冲通道的基本使用:展示了发送和接收操作必须同步进行的特性。
  2. 利用无缓冲通道实现协程同步:通过空结构体信号,让主协程等待子协程执行完毕。
  3. 通道的关闭与数据消费:演示了如何关闭通道以及用 range 自动遍历数据,避免因死锁而阻塞。
  4. 有缓冲通道的使用:展示了带缓冲区通道在生产者与消费者场景中的使用,以及缓冲满、缓冲空时的阻塞机制。
  5. select 监听多个通道:通过 select 实现多路复用、超时控制以及非阻塞检测。
相关推荐
uhakadotcom1 分钟前
Tableau入门:数据可视化的强大工具
后端·面试·github
程序员总部17 分钟前
单例模式在Python中的实现和应用
开发语言·python·单例模式
demonlg011220 分钟前
Go 语言 fmt 模块的完整方法详解及示例
开发语言·后端·golang
程序员鱼皮28 分钟前
2025 年最全Java面试题 ,热门高频200 题+答案汇总!
java·后端·面试
测试盐30 分钟前
django入门教程之cookie和session【六】
后端·python·django
冷琴199632 分钟前
基于python+django的商城网站-电子商城管理系统源码+运行
开发语言·python·django
天草二十六_简村人1 小时前
Rabbitmq消息被消费时抛异常,进入Unacked 状态,进而导致消费者不断尝试消费(下)
java·spring boot·分布式·后端·rabbitmq
Tadecanlan1 小时前
[C++面试] 你了解视图吗?
开发语言·c++
uhakadotcom1 小时前
APM系统简介及案例
后端·面试·github