go语言学习笔记 — 进阶 — 并发编程(7):通道(channel) —— 各种各样的通道

单向通道

在声明通道时,我们可以设置只发送或只接收。这种被约束操作方向的通道称为单向通道。

  • 声明单向通道

只发送:chan<-,只接收:<-chan

var 通道实例 chan<- 元素类型  // 只发送数据

var 通道实例 <-chan 元素类型  // 只接受数据

通道实例即通道变量;元素类型为通道传输的数据类型。

  • 单向通道使用实例

只能发送数据的通道类型为chan<-

ch := make(chan int)  // 创建一个通道实例

var chSendOnly chan<- int = ch // 只发送数据,并赋值ch

只能接收数据的通道类型为<-chan

ch := make(chan int)  // 创建一个通道实例

var chRecvOnly <-chan int = ch // 只接受数据,并赋值ch

使用make创建只发数据或只读数据的通道

ch := make(<-chan int)

var chReadOnly <-chan int = ch
<-chReadOnly

一个不能填充数据(发送数据)的通道是无意义的。


带缓冲的通道

  • 创建带缓冲的通道
通道实例 := make(chan 通道元素类型, 缓冲大小)

通道元素类型:指定通道传输元素的类型
缓冲大小:指定通道最多可保存的元素数量
通道实例:创建出的通道变量

package main

import (
	"fmt"
)

func main() {
	ch := make(chan int, 3) // 创建有3个整型元素缓冲大小的通道

	fmt.Println(len(ch))   // 查看当前通道大小,0

	ch <- 1 // 发送3个整型元素到通道,由于使用了缓冲通道,即便没有goroutine接收数据,发送者也不会阻塞
	ch <- 2
	ch <- 3

	fmt.Println(len(ch))  // 查看当前通道大小,3
}
  • 阻塞条件

无缓冲通道可以看作长度永远是0的带缓冲通道。带缓冲通道在下面情况依然会阻塞:(1)当带缓冲通道容量被填满时,尝试再次发送数据时会阻塞;(2)当带缓冲通道容量为空时,尝试接收数据时会阻塞。

  • 带缓冲通道限制缓冲大小

我们知道通道是两个goroutine之间通信的桥梁,使用goroutine的代码必然有一方生产数据,另一方消费数据。当生产者提供数据的速度大于消费者消费数据的速度,如果通道不限制缓冲大小,那么内存会不断膨胀直到应用崩溃。

因此,限制通道的缓冲大小有利于约束数据生产者的供给速度,应该在数据消费者处理量+通道大小的范围内,才能正常处理数据。


通道多路复用

多路复用通常表示在一个通道上传输多路信号或数据流的过程与技术。

报话机同一时刻只能有一边进行收或发的单向通信,电话、网线等是多路复用的双向通信模式。

  • 一般模式同时接收多个通道的数据
for {
    data, ok := <- ch1   // 尝试接收通道1
    
    data, ok := <- ch2   // 尝试接收通道2
    ...                  // 接收其他通道
}
  • 使用select语句同时接收多个通道的数据

go提供select关键字,同时响应多个通道的操作。每个case会对应一个通道的收发过程,当收发完成时,就会触发case中的响应语句。从一个select中,挑选一种case情况进行响应。

select {
  case 操作1:
      响应操作1
  case 操作2:
      响应操作2
  ...
  default:
      没有任何操作时的情况
}

操作1、操作2是通道收发相关的语句。

case语句示例 意义
case <-ch: 接收任意数据
case d :=<-ch: 接收通道变量
case ch<-100: 发送数据

当遇到相关操作发生时,会执行对应的case响应操作。当没有case对应的操作发生时,默认执行default中的语句。


关闭通道后继续使用

通道是一个引用对象,就像map一样。在引用对象没有任何外部引用时,go运行时(runtime)会自动对内存进行垃圾回收(GC)。类似地,通道也可以被主动关闭。

  • 格式

使用close()函数关闭一个通道。关闭的通道仍可以访问,但会有一些问题。

close(ch)
  • 向关闭的通道发数据会触发panic

被关闭的通道不会被置为nil,如果向关闭的通道发送数据,将会触发panic宕机。

package main

import "fmt"

func main() {
    ch := make(chan int)
    
    close(ch)
    
    fmt.Printf("ptr:%p cap:%d len:%d\n", ch, cap(ch), len(ch))
    
    ch <- 1
}
  • 从已关闭的通道接收数据时,将不会发生阻塞
package main

import (
	"fmt"
	"testing"
)

func main() {
	ch := make(chan int, 2) // 创建能保存两个整型元素的通道,缓冲大小为2

	// 在通道中放入两个数据,通道装满了
	ch <- 0
	ch <- 1

	// 关闭通道,此时带缓冲通道的数据不会释放,通道也没消失
	close(ch)

	// 遍历所有缓冲的数据,且多遍历一个
	for i := 0; i < cap(ch)+1; i++ { // cap获取通道的容量;这里故意多取一个元素,造成越界访问
		v, ok := <-ch // 从通道取出数据

		fmt.Println(v, ok) // 打印取出的数据状态
	}
}

/*
0 true
1 true
0 false
*/

运行结果:前两行可以正常输出带缓冲通道的数据,表明带缓冲通道关闭后,仍然可以访问通道内的数据。

第三行,"0 false"表示通道关闭后取出的数据。0表示通道int元素的默认零值,false表示没有取成功,因为此时通道已经空了。我们可以发现,在通道关闭后,即使没有数据了,取出操作也不会发生阻塞,但取出的不是正常传输的数据了。

上一篇:Go:select多路复用选择通道机制


下一篇:Golang 双向, 单向 channel 爬取 股票信息 chan <-, 解析 <-chan