boxmoe_header_banner_img

Hello! 欢迎来到我的博客!

加载中

文章导读

Go Channel – 协程通信机制


avatar
xiaoifei 2026年5月14日 16

参考资料:
https://www.bilibili.com/video/BV1gf4y1r79E
https://www.runoob.com/w3cnote/go-channel-intro.html

channels

channel是一种数据类型,他能够实现协程之间的数据通信
他有点类似于操作系统IPC的管道通信方式,但是操作系统的管道通信是单向的,而channel可以是双向也可以是单向,并且channel的数据交换是带有基本类型的

跟map和slices一样,channel在使用前必须经过初始化,并且带有一个基本类型(例如int,float,bool)一同作为类型的声明

ch := make(chan int)       // 缓冲区为0    即 ch := make(chan int, 0) 
ch2 := make(chan int, 100) // 缓冲区为100

创建完毕后,我们就可以使用下面操作符

// goroutine A
ch <- v    // 发送值v到Channel ch中

// goroutine B
v := <-ch  // 从Channel ch中接收数据,并将数据赋值给v

channelType有下面三种(其中ElementType可以为int,float,bool)

  • chan ElementType:可以接收和发送 T类型 的数据
  • chan<- ElementType:只可以用来发送 T 类型 的数据
  • <-chan ElementType:只可以用来接收 T 类型 的数据

channel属于生产者与消费者模型,当生产者生产的东西没人消费,就会阻塞;当消费者需要消费却没有东西,同样会阻塞
也就是channel两侧的协程会保持同步,下面介绍有无缓冲区两种情况对应的例子:

无缓冲区

对于无缓冲区的管道(发送和接收必须同时准备好),当协程A向管道写入,协程B读取时:

  1. 如果A协程运行的慢,还没写入管道;而B协程运行的快,已经从管道另一侧读了。那么由于没有数据可读,B协程会阻塞等待。只有A开始向管道写完后,才会唤起B协程执行后续任务
func main() {
	ch := make(chan int)

	go func() {
		fmt.Println("A: 处理耗时任务中...")
		time.Sleep(2 * time.Second)
		fmt.Println("A: 准备发送")
		ch <- 1
		fmt.Println("A: 发送完成")
	}()

	fmt.Println("B: 准备接收")
	x := <-ch
	fmt.Println("B: 接收到", x)
	
	time.Sleep(time.Second)
}
  1. 如果A协程运行的快,已经开始写入管道;而B协程运行的慢,管道另一侧还没读。那么由于数据没有被消费,A协程会阻塞等待。只有B开始从管道消费完后,才会唤起A协程执行后续任务
func main() {
    ch := make(chan int)

    go func() {
        fmt.Println("A: 准备发送")
        ch <- 1
        fmt.Println("A: 发送完成")
    }()
    
	fmt.Println("B: 处理耗时任务中...")
    time.Sleep(2 * time.Second)
    fmt.Println("B: 准备接收")
    x := <-ch
    fmt.Println("B: 接收到", x)
    
	time.Sleep(time.Second)
}

有缓冲区

  1. A发送时,如果缓冲区没满,发送方会把数据放入缓冲区,然后继续执行,不需要等待接收方
  2. B接收时,如果缓冲区有数据,接收方会直接从缓冲区取出数据,然后继续执行。
func main() {
	ch := make(chan int, 2)

	go func() {
		fmt.Println("A: 准备发送 1")
		ch <- 1
		fmt.Println("A: 发送 1 完成")

		fmt.Println("A: 准备发送 2")
		ch <- 2
		fmt.Println("A: 发送 2 完成")
	}()

	time.Sleep(2 * time.Second)

	fmt.Println("B: 准备接收")
	x := <-ch
	fmt.Println("B: 接收到", x)
	
	time.Sleep(time.Second)
}

  1. 发送时,如果缓冲区已经满了,发送方会阻塞,直到有接收方取走数据,缓冲区空出位置
func main() {
	ch := make(chan int, 2)

	go func() {
		fmt.Println("A: 发送 1")
		ch <- 1

		fmt.Println("A: 发送 2")
		ch <- 2

		fmt.Println("A: 发送 3")
		// 阻塞直到B读取
		ch <- 3

		fmt.Println("A: 发送 3 完成")
	}()

	time.Sleep(2 * time.Second)

	fmt.Println("B: 准备接收")
	x := <-ch
	fmt.Println("B: 接收到", x) // 1

	time.Sleep(time.Second)
}
  1. 接收时,如果缓冲区为空,接收方会阻塞,直到有发送方发送数据。(略)

关闭channel

close(ch)

使用场景:

  1. 不需要像文件一样每次都显示关闭协程,一般不会手动关闭
  2. 只有当另一个协程处于循环+阻塞等待时候,当前协程才需要显示关闭,释放资源

注意:

  1. channel手动关闭后,协程不能写数据,但是可以读数据
  2. 对于nil的channel,无论怎么收发都会阻塞

当协程中有循环依赖channel返回的结果时候

func main() {
	ch := make(chan int)

	go func() {
		for i := 0; i < 5; i++ {
			ch <- i
			fmt.Println("B:已发送: ", i)
		}

		close(ch)  // 必须进行关闭
	}()

	for {
		if data, ok := <-ch; ok {
			fmt.Println("A:已接收: ", data)
		} else {
			break
		}
	}

	fmt.Println("main exit")
}

如果去掉close将会引发panic:fatal error: all goroutines are asleep - deadlock!


有缓冲区,并且缓冲区没有被消费完时,close调channel会不会导致ok值变化?

func main() {
	ch := make(chan int, 6)

	go func() {
		for i := 0; i < 5; i++ {
			ch <- i
			fmt.Println("B:已发送: ", i)
		}

		close(ch)
	}()

	time.Sleep(2 * time.Second)

	for {
		if data, ok := <-ch; ok {
			fmt.Println("A:已接收: ", data)
		} else {
			break
		}
	}

	fmt.Println("main exit")
}

为了保证接收前B发送完毕,我们让main睡了2秒,可以看到

ok依旧在缓冲区有值的时候为true
只有缓冲区没有数据的时候,并且channel已经被关闭的清空下,ok才会为false

使用

Range迭代读取数据

for data := range ch{
	fmt.Println("A:已接收: ", data)
}

该段代码会不断从ch读取数据,如果没有缓冲区数据并且已经channel已关闭则会退出

select监控多个channel读写状态

select{
	case <- chanA:
	//如果chanA成功读到数据,则进行该case处理语句
case chanB <- 1:
	//如果成功向chanB写入数据,则进行该case处理语句
default:
	//如果上面都没有成功,则进入default处理流程

案例:

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)

	go func() {
		time.Sleep(2 * time.Second)
		ch1 <- "接口 A 的结果"
	}()

	go func() {
		time.Sleep(1 * time.Second)
		ch2 <- "接口 B 的结果"
	}()

	select {
	case result := <-ch1:
		fmt.Println("收到 ch1:", result)

	case result := <-ch2:
		fmt.Println("收到 ch2:", result)
	}

	fmt.Println("main 结束")
}

如果同时准备好,那么sleclet会随机选择一个可以执行的case

超时控制

// 创建计时器
timerCh := time.After(2 * time.Second)

执行上面的函数会返回一个 <-chan time.Time 他是一个只读的管道,并且在两秒之后会往 timerCh 里面发送一个时间戳

我们可以搭配 select 做超时控制

func main() {
	resultCh := make(chan string)

	go func() {
		time.Sleep(3 * time.Second)
		resultCh <- "任务结果"
	}()

	select {
	case result := <-resultCh:
		fmt.Println("收到结果:", result)

	case <-time.After(2 * time.Second): // 2s后会读到
		fmt.Println("等待超时")
	}
}
Go


评论(0)

查看评论列表

暂无评论


发表评论

表情 颜文字
插入代码