Go 学习笔记(24)— 并发(03)[通道特点、通道声明、通道发送/接收/关闭、单向通道]

1. 通道概念

chanGo 语言里面的一个关键宇,是 channel 的简写,翻译为中文就是通道。

goroutineGo 语言里面的并发执行体,通道是 goroutine 之间通信和同步的重要组件。 Go 的哲学是

Don’t communicate by sharing memory; share memory by communicating. (不要通过共享内存来通信,而应该通过通信来共享内存。)这是作为 Go 语言的主要创造者之一的 Rob Pike 的至理名言。

Go 语言通过通道可以实现多个 goroutine 之间内存共享。

Go 语言里,你不仅可以使用原子函数和互斥锁来保证对共享资源的安全访问以及消除竞争状态,还可以使用通道,通过发送和接收需要共享的资源,在 goroutine 之间做同步。

当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了确保同步交换数据的机制。

多个 goroutine 中, Go 语言使用通道( channel )进行通信,通道是一种内置的数据结构,可以让用户在不同的 goroutine 之间同步发送具有类型的消息。这让编程模型更倾向于在 goroutine 之间发送消息,而不是让多个 goroutine 争夺同一个数据的使用权。

程序可以将需要并发的环节设计为生产者模式和消费者的模式,将数据放入通道。通道另外一端的代码将这些数据进行并发计算并返回结果,如下图所示。

生产消费者

生产者和消费者代码示例:

// 整段代码中,没有线程创建,没有线程池也没有加锁,
// 仅仅通过关键字 go 实现 goroutine,和通道实现数据交换。
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 customer(channel <-chan string) {
	// 不停地获取数据
	for {
		// 从通道中取出数据, 此处会阻塞直到信道中返回数据
		message := <-channel
		// 打印数据
		fmt.Println(message)
	}
}
func main() {
	// 创建一个字符串类型的通道
	channel := make(chan string)
	// 创建producer()函数的并发goroutine
	go producer("cat", channel)
	go producer("dog", channel)
	// 数据消费函数
	customer(channel)
}

声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。

2. 通道特点

  • 通道类型的值本身就是并发安全的,这也是 Go 语言自带的、唯一一个可以满足并发安全性的类型。

  • 一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值一定会先被接收。

  • 在任何时候,同时只能有一个 goroutine访问通道进行发送和获取数据。 goroutine间通过通道就可以通信。

  • 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。

  1. 在同一时刻,Go 语言的运行时系统(以下简称运行时系统)只会执行对同一个通道的任意个发送操作中的某一个。直到这个元素值被完全复制进该通道之后,其他针对该通道的发送操作才可能被执行。
  2. 类似的,在同一时刻,运行时系统也只会执行对同一个通道的任意个接收操作中的某一个。直到这个元素值完全被移出该通道之后,其他针对该通道的接收操作才可能被执行。
  3. 即使这些操作是并发执行的也是如此。这里所谓的并发执行,你可以这样认为,多个代码块分别在不同的 goroutine 之中,并有机会在同一个时间段内被执行。
  4. 另外,对于通道中的同一个元素值来说,发送操作接收操作之间也是互斥的。例如,虽然会出现,正在被复制进通道但还未复制完成的元素值,但是这时它绝不会被想接收它的一方看到和取走。
  5. 这里要注意的一个细节是,元素值从外界进入通道时会被复制。更具体地说,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本
  6. 另一方面,元素值从通道进入外界时会被移动。这个移动操作实际上包含了两步,
    (a)、第一步是生成正在通道中的这个元素值的副本,并准备给到接收方;
    (b)、第二步是删除在通道中的这个元素值;
  • 发送操作和接收操作中对元素值的处理都是不可分割的。

这里的“不可分割”的意思是,它们处理元素值时都是一气呵成的,绝不会被打断。例如:

  1. 发送操作要么还没复制元素值,要么已经复制完毕,绝不会出现只复制了一部分的情况。
  2. 接收操作在准备好元素值的副本之后,一定会删除掉通道中的原值,绝不会出现通道中仍有残留的情况。

这既是为了保证通道中元素值的完整性,也是为了保证通道操作的唯一性。对于通道中的同一个元素值来说,它只可能是某一个发送操作放入的,同时也只可能被某一个接收操作取出。

  • 发送操作在完全完成之前会被阻塞。接收操作也是如此。
  1. 发送操作 包括了 复制元素值放置副本到通道内部 这两个步骤。在这两个步骤完全完成之前,发起这个发送操作的那句代码会一直阻塞在那里。
    也就是说,在它之后的代码不会有执行的机会,直到这句代码的阻塞解除。
    更细致地说,在通道完成发送操作之后,运行时系统会通知这句代码所在的 goroutine,以使它去争取继续运行代码的机会。

  2. 接收操作 通常包含了 复制通道内的元素值放置副本到接收方删掉原值 三个步骤,也就是说通常,值进入通道时会被复制一次,然后出通道的时候依照通道内的那个值再被复制一次并给到接收方。在所有这些步骤完全完成之前,发起该操作的代码也会一直阻塞,直到该代码所在的 goroutine 收到了运行时系统的通知并重新获得运行机会为止。

3. 通道声明

Go 语言提倡使用通信的方法代替共享内存,当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了确保同步交换数据的机制。

一般 channel 的声明形式为:

var chanName chan ElementType

与一般的变量声明不同的地方仅仅是在类型之前加了 chan 关键字。ElementType 指定这个
channel 所能传递的元素类型。

声明一个传递类型为 intchannel

var ch chan int

或者使用内置函数 make 来创建一个通道,


// 无缓冲的整型通道
unbuffered := make(chan int)

// 有缓冲的字符串通道
buffered := make(chan string, 10)

make 的第一个参数需要是关键字 chan ,之后跟着允许通道交换的数据的类型。如果创建的是一个有缓冲的通道,之后还需要在第二个参数指定这个通道的缓冲区的大小。

Go 提供内置函数 lencap ,无缓冲的通道的 lencap 都是 0,有缓冲的通道的 len 代表没有被读取的元素数, cap 代表整个通道的容量。

d1 := make(chan int)
d2 := make(chan int, 3)
d2 <- 1
fmt.Println(len(d1), cap(d1)) // 0 0
fmt.Println(len(d2), cap(d2)) // 1 3

无缓冲的通道既可以用于通信,也可以用于两个goroutine 的同步,有缓冲的通道主要用于通信。

map 类似, channel 也对应一个 make 创建的底层数据结构的引用。当我们复制一个 channel 或用于函数参数传递时,我们只是拷贝了一个 channel 引用,因此调用者和被调用者将引用同一个 channel 对象。和其它的引用类型一样, channel 的零值也是 nil

声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。

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

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

4. 通道操作

发送和接收两个操作都使用 <- 运算符。在发送语句中, <- 运算符分割 channel 和要发送的值。在接收语句中, <- 运算符写在 channel 对象之前。

4.1 通道发送数据

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

channelName <- value 

其中:

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

使用 make 创建一个通道后,就可以使用<-向通道发送数据,代码如下:

// 有缓冲的字符串通道,数据类型是字符串,包含一个 10 个值的缓冲区。
buffered := make(chan string, 10)

// 通过通道发送一个字符串
buffered <- "Gopher"

4.2 通道发送阻塞

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

package main

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

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

输出:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:

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

但是如果是有缓冲的通道时则不会发生阻塞,见下代码:

package main

func main() {
	ch := make(chan int, 10)	// 有缓冲的通道
	ch <- 0
}

4.3 通道接收数据

一个 channel 有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个 goroutine 通过 channel 发送到另一个执行接收操作的 goroutine

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

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

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

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

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

每次接收一个元素。

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

通道接收数据有以下几种写法。

4.3.1 阻塞接收数据

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

data := <-ch

为了让另一个 goroutine 可以从该通道里接收到这个字符串,我们依旧使用 <- 操作符,但这次是一元运算符,

// 有缓冲的字符串通道,数据类型是字符串,包含一个 10 个值的缓冲区。
buffered := make(chan string, 10)

// 通过通道发送一个字符串
buffered <- "Gopher"
// 从通道接收一个字符串
value := <-buffered

当从通道里接收一个值或者指针时, <- 运算符在要操作的通道变量的左侧。

4.3.2 非阻塞接收数据

使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下:

data, ok := <- ch
  • data:表示接收到的数据。未接收到数据时,data 为通道类型的零值。
  • ok:表示是否接收到数据。

非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。如果需要实现接收超时检测,可以配合 select 和计时器 channel 进行。

package main

import (
	"errors"
	"fmt"
	"time"
)

// 模拟RPC客户端的请求和接收消息封装
func RPCClient(ch chan string, req string) (string, error) {
	// 向服务器发送请求
	ch <- req

	// 等待服务器返回
	select {
	// 下面两个通道操作同时开启,那个先返回就先执行哪个后面的语句 
	case ack := <-ch: // 接收到服务器返回数据
		return ack, nil
	case <-time.After(time.Second): // 超时
		return "", errors.New("Time out")
	}
}

// 模拟RPC服务器端接收客户端请求和回应
func RPCServer(ch chan string) {
	for {
		// 接收客户端请求
		data := <-ch
		// 打印接收到的数据
		fmt.Println("server received:", data)
		time.Sleep(2 * time.Second)
		// 反馈给客户端收到
		ch <- "roger"
	}
}

func main() {

	// 创建一个无缓冲字符串通道
	ch := make(chan string)
	// 并发执行服务器逻辑
	go RPCServer(ch)
	// 客户端请求数据和接收数据
	recv, err := RPCClient(ch, "hi")
	if err != nil {
		// 发生错误打印
		fmt.Println(err)
	} else {
		// 正常接收到数据
		fmt.Println("client received", recv)
	}
}

更多 select 操作请参考 https://blog.csdn.net/wohu1104/article/details/115497151

4.3.3 接收任意数据,忽略接收的数据

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

<-ch	//	a	receive	statement;	result	is	discarded

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

使用通道做并发同步的写法,可以参考下面的例子:

package main

import "fmt"

func main() {
	// 构建一个通道
	ch := make(chan int)
	// 开启一个并发匿名函数
	go func() {
		fmt.Println("start goroutine")
		// 通过通道通知main的goroutine
		ch <- 0
		fmt.Println("exit goroutine")
	}()

	fmt.Println("wait goroutine")
	// 等待匿名goroutine
	<-ch
	fmt.Println("all done")
}

输出结果:

wait goroutine
start goroutine
exit goroutine
all done

4.3.4 循环接收

通道的数据接收可以借用 for range 语句进行多个元素的接收操作,格式如下:

for data := range ch {
    
}

range 子句的迭代目标不能是一个发送通道,与试图从发送通道接收元素值情况一样,这会造成编译错误。

for ... range 循环遍历通道时,信道必须关闭,否则会引发 deadlock 错误。

迭代为 nil 的通道值会让当前流程永远阻塞在 for 语句上。

func main() {
	// 创建一个channel
	var c chan int
	for data := range c {
		fmt.Println(data)
	}
}

运行错误提示:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive (nil chan)]:
main.main()
	/home/wohu/gocode/src/100.go:8 +0xa8
exit status 2

通道 ch 是可以进行遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。通过 for 遍历获得的变量只有一个,即上面例子中的 data。

遍历通道数据的例子请参考下面的代码:

// 使用匿名函数
package main

import (
	"fmt"
	"time"
)

func main() {
	// 构建一个通道
	ch := make(chan int)
	// 开启一个并发匿名函数
	go func() {
		// 从3循环到0
		for i := 3; i >= 0; i-- {
			// 发送3到0之间的数值
			ch <- i
			// 每次发送完时等待
			time.Sleep(time.Second)
		}
	}()

	// 遍历接收通道数据
	for data := range ch {
		// 打印通道数据
		fmt.Println(data)
		// 当遇到数据0时, 退出接收循环
		if data == 0 {
			break
		}
	}

}

// 非匿名函数
package main

import (
	"fmt"
	"time"
)

func getNumber(ch chan<- int) {
	// 从3循环到0
	for i := 3; i >= 0; i-- {
		// 发送3到0之间的数值
		ch <- i
		// 每次发送完时等待
		time.Sleep(time.Second)
	}
}
func main() {
	// 构建一个通道
	ch := make(chan int)
	// 开启一个并发匿名函数
	go getNumber(ch)

	// 遍历接收通道数据
	for data := range ch {
		// 打印通道数据
		fmt.Println(data)
		// 当遇到数据0时, 退出接收循环
        // 如果继续发送,由于接收 goroutine 已经退出,没有 goroutine 发送到通道,因此运行时将会触发宕机报错。
		if data == 0 {
			break
		}
	}

}

5. 通道关闭

关闭通道一般都在发送端关闭。

手动关闭通道是个很好的习惯,而且也可以利用关的动作来给接收方传递一个信号。GoGC 只会清理被分配到堆上的、不再有任何引用的对象。

channel 支持 close 操作,用于关闭 channel ,随后对基于该 channel 的任何发送操作都将导致 panic 异常。

对一个已经被 close 过的 channel 进行接收操作依然可以接受到之前已经成功发送的数据;如果 channel 中已经没有数据的话将产生一个零值的数据。

使用内置的 close 函数就可以关闭一个 channel :

close(ch)

如何判断一个 channel 是否已经被关闭?我们可以使用 channelreceive 支持 multi-valued assignment (多值赋值),如

v,	ok	:=	<-ch

只需要看第二个 bool 返回值即可,如果返回值是 false 则表示 ch 已经被关闭。它可以用来检查 channel 是否已经被关闭了。

5.1 向已关闭通道发送数据触发 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
}

输出结果:

ptr:0xc000058060 cap:0 len:0
panic: send on closed channel

5.2 从已关闭的通道接收数据时将不会发生阻塞

从已经关闭的通道接收数据或者正在接收数据时,将会接收到通道类型的零值,然后停止阻塞并返回。代码如下:

package main

import "fmt"

func main() {
	// 创建一个整型带两个缓冲的通道
	ch := make(chan int, 2)
	// 给通道放入两个数据
	ch <- 0
	ch <- 1
	// 关闭缓冲
	close(ch)

	// 遍历缓冲所有数据, 且多遍历1个
	for i := 0; i < cap(ch)+1; i++ {
		// 从通道中取出数据
		v, ok := <-ch
		// 打印取出数据的状态
		fmt.Println(v, ok)
	}
}

输出结果:

0 true
1 true
0 false // 表示通道在关闭状态下取出的值。0 表示这个通道的默认值,false 表示没有获取成功

6. 单向通道

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

单向通道的声明格式:

单向 channel 变量的声明非常简单,只能发送的通道类型为chan<-,只能接收的通道类型为<-chan,格式如下:

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

通道必须用 make 初始化后才能使用,关于读写顺序可以采用下面的方法联想记忆。
读在前,写在后 <- chan 中的 <- 符号在 chan 前,因此为只读通道;chan <- 中的 <- 符号在 chan 后,因此为 只写通道

示例:

// 只能发不能收的通道。
var uselessChan = make(chan<- int, 1)
// 只能收不能发的通道。
var anotherUselessChan = make(<-chan int, 1)

var ch1 chan int // ch1是一个正常的channel,不是单向的
var ch2 chan<- float64// ch2是单向channel,只用于写float64数据
var ch3 <-chan int // ch3是单向channel,只用于读取int数据
  • 元素类型:通道包含的元素类型。
  • 通道实例:声明的通道变量。

使用示例:

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
d := (chan int)(recv) // Error: cannot convert type <-chan int to type chan int

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

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

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

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

双向 channel 转化为单向 channel 之间进行转换。示例如下:

ch4 := make(chan int)
ch5 := <-chan int(ch4) // ch5就是一个单向的读取channel
ch6 := chan<- int(ch4) // ch6 是一个单向的写入channel

单向通道使用示例:

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("cat", channel)
	go producer("dog", channel)
	// 数据消费函数
	consumer(channel)
}

7. 通道示例

使用无缓冲通道往里面装入数据时,装入方将被阻塞,直到另外通道在另外一个 goroutine 中被取出。同样,如果通道中没有放入任何数据,接收方试图从通道中获取数据时,同样也是阻塞。发送和接收的操作是同步完成的。

package main

import "fmt"

func printer(c chan int) {
	// 开始无限循环等待数据
	for {
		// 从channel中获取一个数据
		data := <-c
		// 将0视为数据结束
		if data == 0 {
			break
		}
		// 打印数据
		fmt.Println(data)
	}
	// 通知main已经结束循环(我搞定了!)
	c <- 0
}

func main() {
	// 创建一个channel
	c := make(chan int)
	// 并发执行printer, 传入channel
	go printer(c)
	for i := 1; i <= 10; i++ {
		// 将数据通过channel投送给printer
		c <- i
	}
	// 通知并发的printer结束循环(没数据啦!)
	c <- 0
	// 等待printer结束(搞定喊我!)
	<-c
}

8. 发送操作和接收操作在什么时候可能被长时间的阻塞?

8.1 对缓冲通道而言

针对缓冲通道的情况。如果通道已满,那么对它的所有发送操作都会被阻塞,直到通道中有元素值被接收走。

这时,通道会优先通知最早因此而等待的、那个发送操作所在的 goroutine,后者会再次执行发送操作。

由于发送操作在这种情况下被阻塞后,它们所在的 goroutine 会顺序地进入通道内部的发送等待队列,所以通知的顺序总是公平的。

相对的,如果通道已空,那么对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。

这时,通道会通知最早等待的那个接收操作所在的 goroutine,并使它再次执行接收操作。

因此而等待的、所有接收操作所在的 goroutine,都会按照先后顺序被放入通道内部的接收等待队列。

8.2 对非缓冲通道而言

对于非缓冲通道,情况要简单一些。无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。

由此可见,非缓冲通道是在用同步的方式传递数据。也就是说,只有收发双方对接上了,数据才会被传递。并且,数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。

相比之下,缓冲通道则在用异步的方式传递数据。在大多数情况下,缓冲通道会作为收发双方的中间件。

正如前文所述,元素值会先从发送方复制到缓冲通道,之后再由缓冲通道复制给接收方。但是,当发送操作在执行的时候发现空的通道中,正好有等待的接收操作,那么它会直接把元素值复制给接收方。

以上说的都是在正确使用通道的前提下会发生的事情。下面我特别说明一下,由于错误使用通道而造成的阻塞。

对于值为 nil 的通道,不论它的具体类型是什么,对它的发送操作和接收操作都会永久地处于阻塞状态。它们所属的 goroutine 中的任何代码,都不再会被执行。

注意,由于通道类型是引用类型,所以它的零值就是 nil 。换句话说,当我们只声明该类型的变量但没有用 make 函数对它进行初始化时,该变量的值就会是 nil。我们一定不要忘记初始化通道!

package main

func main() {
	// 示例1。
	ch1 := make(chan int, 1)
	ch1 <- 1
	//ch1 <- 2 // 通道已满,因此这里会造成阻塞。

	// 示例2。
	ch2 := make(chan int, 1)
	//elem, ok := <-ch2 // 通道已空,因此这里会造成阻塞。
	//_, _ = elem, ok
	ch2 <- 1

	// 示例3。
	var ch3 chan int
	//ch3 <- 1 // 通道的值为nil,因此这里会造成永久的阻塞!
	//<-ch3 // 通道的值为nil,因此这里会造成永久的阻塞!
	_ = ch3
}

9. 发送操作和接收操作在什么时候会引发 panic?

对于一个已初始化,但并未关闭的通道来说,收发操作一定不会引发 panic

但是通道一旦关闭,再对它进行发送操作,就会引发 panic

另外,如果我们试图关闭一个已经关闭了的通道,也会引发 panic

注意,接收操作是可以感知到通道的关闭的,并能够安全退出。更具体地说,当我们把接收表达式的结果同时赋给两个变量时,第二个变量的类型就是一定 bool 类型。它的值如果为 false 就说明通道已经关闭,并且再没有元素值可取了。

注意,如果通道关闭时,里面还有元素值未被取出,那么接收表达式的第一个结果,仍会是通道中的某一个元素值,而第二个结果值一定会是 true 。因此,通过接收表达式的第二个结果值,来判断通道是否关闭是可能有延时的。

由于通道的收发操作有上述特性,所以除非有特殊的保障措施,我们千万不要让接收方关闭通道,而应当让发送方做这件事。

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值