likes
comments
collection
share

Golang之Channel详细介绍

作者站长头像
站长
· 阅读数 26

一、概述

​ 通道(Channel)是 Golang 在语言级别上提供的 goroutine 间的通讯方式,可以使用channel在多个 goroutine 之间传递消息。如果说 goroutine 是 Go 程序并发的执行体,channel 就是它们之间的连接。channel 是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。

​ Golang 的并发模型是 CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信

​ Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明 channel 的时候需要为其指定元素类型。

二、通道的声明

channel是 Go 语言中一种特有的类型。

声明通道类型变量的格式如下:

var 变量名称 chan 元素类型

说明

  • 变量名称:保存通道的变量
  • chan:声明通道的关键字
  • 元素类型:是指通道中传递元素的类型

注意:chan 类型的空值是 nil,声明后需要配合 make 后才能使用。

示例

var ch1 chan int // 声明一个传递整型的通道

var ch2 chan bool // 声明一个传递布尔型的通道

var ch3 chan []int // 声明一个传递 int 切片的通道

三、创建通道

通道是引用类型,需要使用 make 进行创建。

格式如下:

通道实例 := make(chan 元素类型, [缓冲大小])

说明

  • channel的缓冲大小是可选的。

示例

ch1 := make(chan int,10)                 // 创建一个能存储10个int类型数据的通道

ch2 := make(chan []int, 3)				//创建一个能存储3个[]int 切片类型数据的通道

ch3 := make(chan interface{})         // 创建一个空接口类型的通道, 可以存放任意格式

type Equip struct{ /* 一些字段 */ }
ch4 := make(chan *Equip)             // 创建Equip指针类型的通道, 可以存放*Equip

四、channel操作

通道共有发送(send)、接收(receive)和关闭(close)三种操作。

4.1 使用通道发送数据

通道创建后,就可以使用通道进行发送和接收操作。

(1)通道发送数据的格式

通道的发送使用特殊的操作符<-,将数据通过通道发送的格式为:

通道变量 <- 值
  • 通道变量:通过make创建好的通道实例。
  • :可以是变量、常量、表达式或者函数返回值等。值的类型必须与ch通道的元素类型一致。

示例

func main() {
	//创建通道ch
	ch := make(chan int,2)
	//从通道里面发送数据
	ch <- 10	//将10发送到通道ch中
	ch <- 20	//将20发送到通道ch中

	// 创建一个空接口通道ch1
	ch1 := make(chan interface{},2)
	ch1 <- 10	// 将10放入通道中
	ch1 <- "hello"	// 将hello字符串放入通道中
}

(2)发送将持续阻塞直到数据被接收

把数据往通道中发送时,如果接收方一直都没有接收,那么发送操作将持续阻塞。

Go 程序运行时能智能地发现一些永远无法发送成功的语句并做出提示,代码如下:

func main() {
    // 创建一个整型通道
    ch := make(chan int)

    // 尝试将0通过通道发送
    ch <- 0
}

运行代码报如下错误

fatal error: all goroutines are asleep - deadlock!

报错的意思是:运行时发现所有的 goroutine(包括main)都处于等待 goroutine。也就是说所有 goroutine 中的 channel 并没有形成发送和接收对应的代码。

4.2 使用通道接收数据

通道接收同样使用<-操作符,通道接收有如下特性:

  • 通道的收发操作在不同的两个 goroutine 间进行

    由于通道的数据在没有接收方处理时,数据发送方会持续阻塞,因此通道的接收必定在另外一个 goroutine 中进行。

  • 接收将持续阻塞直到发送方发送数据

    如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止。

  • 每次接收一个元素

    通道一次只能接收一个数据元素。

通道的数据接收一共有以下 4 种写法:

(1)阻塞接收数据

阻塞模式接收数据时,将接收变量作为<-操作符的左值,格式如下:

data := <-ch

执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量。

(2)接收任意数据,忽略接收的数据

阻塞接收数据后,忽略从通道返回的数据,格式如下:

<-ch

执行该语句时将会发生阻塞,直到接收到数据,但接收到的数据会被忽略。这个方式实际上只是通过通道在 goroutine 间阻塞收发实现并发同步。

4.3 关闭通道

可以通过调用内置的close函数来关闭通道。

close(ch)

**注意:**一个通道值是可以被垃圾回收掉的。通道通常由发送方执行关闭操作,并且只有在接收方明确等待通道关闭的信号时才需要执行关闭操作。它和关闭文件不一样,通常在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

  1. 对一个关闭的通道再发送值就会导致 panic。
  2. 对一个关闭的通道进行接收会一直获取值直到通道为空。
  3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  4. 关闭一个已经关闭的通道会导致 panic。

多返回值模式

当一个通道被关闭后,再往该通道发送值会引发panic,从该通道取值的操作会先取完通道中的值。通道内的值被接收完后再对通道执行接收操作得到的值会一直都是对应元素类型的零值。

对一个通道执行接收操作时支持使用如下多返回值模式。

value, ok := <- ch

说明

  • value:从通道中取出的值,如果通道被关闭则返回对应类型的零值。
  • ok:通道ch关闭时返回 false,否则返回 true。

示例

定义一个函数循环从通道中接收所有值,直到通道被关闭后退出

func fun(ch chan int) {
	for {
		value, ok := <-ch
		if !ok {
			fmt.Println("通道已关闭")
			break
		}
		fmt.Printf("value:%#v,ok:%#v\n", value, ok)
	}
}

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	close(ch)
	fun(ch)
}

运行结果

value:1,ok:true
value:2,ok:true
通道已关闭

五、通道的容量与长度

在上面介绍的创建通道时,传入的第二个参数表示通道的容量。

  • 当容量为 0 时,说明通道中不能存放数据,在发送数据时,必须要求立马有人接收,否则会报错。此时的通道称之为无缓冲通道。
  • 当容量为 1 时,说明通道只能缓存一个数据,若通道中已有一个数据,此时再往里发送数据,会造成程序阻塞。利用这点可以利用通道来做锁。
  • 当容量大于 1 时,通道中可以存放多个数据,可以用于多个协程之间的通信管道,共享资源。

示例

可以通过 cap 函数和 len 函数获取通道的容量和长度。

func main() {
	// 创建一个通道
	ch := make(chan int, 3)
	fmt.Println("刚创建成功后:")
	fmt.Printf("cap = %v,len = %v \n", cap(ch),len(ch))	//cap = 3,len = 0 
	ch <- 1
	ch <- 2
	fmt.Println("向通道中传入两个参数后:")
	fmt.Printf("cap = %v,len = %v \n", cap(ch),len(ch))	//cap = 3,len = 2 
	<- ch
	fmt.Println("从通道中取出一个值后:")
	fmt.Printf("cap = %v,len = %v \n", cap(ch),len(ch))	//cap = 3,len = 1 
}

六、通道阻塞

6.1 无缓冲通道

如果创建通道的时候没有指定容量,那么可以叫这个通道为无缓冲的通道。无缓冲的通道又称为阻塞的通道。

示例

func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Println("发送成功")
}

上面这段代码能够通过编译,但是执行的时候会出现以下错误:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	D:/Codes/gocode/godemo/base10/test04.go:7 +0x31

无缓冲通道在通道里无法存储数据,接收端必须先于发送端准备好,以确保你发送完数据后,有人立马接收数据,否则发送端就会造成阻塞,原因很简单,通道中无法存储数据。也就是说发送端和接收端是同步运行的。

6.2 有缓冲通道

在使用make 函数初始化通道的时候为其指定通道的容量。

func main() {
    ch := make(chan int, 1) // 创建一个容量为 1 的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的最大数量。

七、 遍历通道

通常会选择使用for range循环从通道中接收值,当通道被关闭后,会在通道内的所有值被接收完毕后会自动退出循环。

示例

func fun2(ch chan int) {
	//通过for range遍历通道中的值
	for v := range ch {
		fmt.Println(v)
	}
}

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	close(ch)
	fun2(ch)
}

运行结果

1
2

八、单向通道

8.1 单向通道概述

Go 语言的类型系统提供了单方向的 channel 类型,顾名思义,单向 channel 只能用于发送或者接收数据。 channel 本身必然是同时支持读写的,否则根本没法用。可以将 channel 隐式转换为单向队列,只收或只发。

单向通道可以分为只读通道只写通道两种。

8.2 单向通道声明

  • 只读通道

只读通道使用<-chan 表示

var 通道实例 <-chan 元素类型    // 只能接收通道

示例

ch1 := make(<-chan int, 2)
//ch1 <- 23
fmt.Println(ch1)
  • 只写通道

只写通道使用chan<- 表示

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

示例

ch2 := make(chan<- int, 2)
ch2 <- 20
//<- ch2 //error
fmt.Println(ch2)

8.3 通道转换

示例

func main() {
	c := make(chan int, 3)
    //只写通道
	var send chan<- int = c // send-only
    //只读通道
	var recv <-chan int = c // receive-only
	send <- 1
	// <-send // Error: receive from send-only type chan<- int
	<-recv
	// recv <- 2 // Error: send to receive-only type <-chan int

	//不能将单向 channel 转换为普通 channel
    //只写通道转双通道
	d := (chan int)(send) // Error: cannot convert type chan<- int to type chan int
    //只读通道转双通道
	r := (chan int)(recv) // Error: cannot convert type <-chan int to type chan int
}

从示例可以看出,不能将单向 channel 转换为普通 channel。

一般在通道的声明时,都不会刻意声明为单通道,这样做会声明一个只进不出,或者只出不进的单通道,没有任何意义

而是在函数的定义形参过程中指定通道的是发送通道还是接收通道。这样做的目的适用于约束其他代码的行为。

func SendInt(ch chan<- int) {
  ch <- rand.Intn(100)
}

这个函数只接受一个 chan<- int 类型的参数。在这个函数中的代码只能向参数 ch 发送元素值,而不能从它那里接收元素值。这就起到了约束函数行为的作用。

双向 channel 转化为单向 channel 之间进行转换。

示例

ch := make(chan int)
ch1 := <-chan int(ch) // ch1 是一个单向的读取channel
ch2 := chan<- int(ch) // ch2 是一个单向的写入channel

8.4 单向通道使用示例

package main

import (
	"fmt"
	"math/rand"
	"time"
)

// 数据生产者
func producer(header string, channel chan<- string) {
	// 无限循环,不停的生产数据
	for {
		// 将随机数和字符串格式化为字符串发送到通道
		channel <- fmt.Sprintf("%s: %v", header, rand.Int31())
		// 等待1秒
		time.Sleep(time.Second)
	}
}

// 数据消费者
func consumer(channel <-chan string) {
	// 不停的获取数据
	for {
		// 从通道中取出数据,此处会阻塞直到信道中返回数据
		message := <-channel
		// 打印数据
		fmt.Println(message)
	}
}

func main() {
	// 创建一个字符串类型的通道
	channel := make(chan string)
	// 创建producer函数的并发goroutine
	go producer("num1", channel)
	go producer("num2", channel)
	// 数据消费函数
	consumer(channel)
}

运行结果

num2: 1298498081
num1: 2019727887
num1: 1427131847
num2: 939984059
num1: 911902081
num2: 1474941318
num2: 336122540
num1: 140954425

九、使用示例

9.1 示例一

需求:定义两个方法,一个方法向通道里面写数据,一个向通道里面读取数据。

要求同步进行

说明

1、开启一个 fn1 的协程向通道 inChan 中写入 100 条数据
2、开启一个 fn2 的协程读取 inChan 中写入的数据
3、注意:fn1 和 fn2 同时操作一个通道
4、主线程必须等待操作完成后才可以退出

代码

import (
	"fmt"
	"sync"
	"time"
)

/**
goroutine结合Channel使用的简单demo,定义两个方法,一个方法给通道里面写数据,一个给通道里面读取数据。要求同步进行。
 */
var wg sync.WaitGroup

//写数据
func fn1(ch chan int){
	for i := 1; i <= 10; i++ {
		ch <- i
		fmt.Printf("写入数据\t%v\t成功\n",i)
		time.Sleep(time.Millisecond * 500)
	}
	close(ch)
	wg.Done()
}

//读数据
func fn2(ch chan int){
	for v := range ch {
		fmt.Printf("读取数据\t%v\t成功\n",v)
		time.Sleep(time.Millisecond * 10)
	}
	wg.Done()
}

func main() {
	var ch = make(chan int,10)

	wg.Add(1)
	go fn1(ch)
	wg.Add(1)
	go fn2(ch)

	wg.Wait()
	fmt.Println("退出...")
}

运行结果

写入数据	1	成功
读取数据	1	成功
读取数据	2	成功
写入数据	2	成功
写入数据	3	成功
读取数据	3	成功
写入数据	4	成功
读取数据	4	成功
写入数据	5	成功
读取数据	5	成功
写入数据	6	成功
读取数据	6	成功
写入数据	7	成功
读取数据	7	成功
写入数据	8	成功
读取数据	8	成功
写入数据	9	成功
读取数据	9	成功
写入数据	10	成功
读取数据	10	成功
退出...

9.2 示例二

需求:goroutine 结合 channel 实现统计 1-120000 的数字中哪些是素数?

代码

import (
	"fmt"
	"sync"
	"time"
)

var wg3 sync.WaitGroup

//向 intChan放入 1-120000个数
func putNum(intChan chan int){
	for i := 2;i < 120000; i++ {
		intChan <- i
	}
	close(intChan)
	wg3.Done()
}

// 从 intChan取出数据,并判断是否为素数,如果是,就把得到的素数放在primeChan
func primeNum(intChan chan int,primeChan chan int,exitChan chan bool){
	for num := range intChan {
		var flag = true
		for i := 2; i < num; i++ {
			if num % i == 0 {
				flag = false
				break
			}
		}
		if flag {
			primeChan <- num	//num是素数
		}
	}
	//要关闭 primeChan
	// close(primeChan) //如果一个channel关闭了就没法给这个channel发送数据了
	//什么时候关闭primeChan?

	//给exitChan里面放入一条数据
	exitChan <- true
	wg3.Done()
}

//printPrime打印素数的方法
func printPrime(primeChan chan int) {
	//for v := range primeChan {
	//	//	fmt.Println(v)
	//	//}
	wg3.Done()
}

func main() {
	start := time.Now().Unix()

	intChan := make(chan int, 1000)
	primeChan := make(chan int, 50000)
	exitChan := make(chan bool,16)	//标识primeChan close

	//存放数字的协程
	wg3.Add(1)
	go putNum(intChan)

	//统计素数的协程
	for i := 0; i < 16; i++ {
		wg3.Add(1)
		go primeNum(intChan,primeChan,exitChan)
	}

	//打印素数的协程
	wg3.Add(1)
	go printPrime(primeChan)

	//判断exitChan是否存满值
	wg3.Add(1)
	go func() {
		for i := 0; i < 16; i++ {
			<- exitChan
		}
		//关闭primeChan
		close(primeChan)
		wg3.Done()
	}()

	wg3.Wait()

	end := time.Now().Unix()
	fmt.Println("执行完毕...",end - start,"毫秒")
}

运行结果

执行完毕... 2 毫秒