管道(channel)的使用

package main

import "fmt"

func main() {
   // 定义一个名为intChan的管道,该管道只能接收int64类型数据,且容量为4(即最多接收4个int64类型数据)
   intChan := make(chan int64, 4) // 等价于:var intChan chan int64 = make(chan int64, 4)

   fmt.Printf("intChan = %+v \n", intChan) // intChan = 0xc0000a4000 重要说明:管道可以看成是一个指针

   // 查询管道的长度和容量(由于还没向管道发送任何数据,此时长度为0)
   fmt.Printf("intChan的长度为:%d,容量为:%d \n", len(intChan), cap(intChan)) // intChan的长度为:0,容量为:4

   // 向管道发送数据
   intChan <- 1 // 发送第1个数据
   intChan <- 0 // 发送第2个数据
   intChan <- 2 // 发送第3个数据
   intChan <- 4 // 发送第4个数据(此时长度已达到容量值,再向管道发送数据就会报错)

   // 下面这行代码是不能执行的,因为上面向管道发送4个数据已经达到容量值,再向管道发送数据就会报fatal error
   // intChan <- 9527 // fatal error: all goroutines are asleep - deadlock!
   // 重要说明:这里务必要理解,报fatal error并不是因为向已写满数据的管道发送数据,而是程序发生了死锁(deadlock)。
   //           向已写满数据的管道发送数据是不会报错的,只是由于没有可用空间存放新数据,发送数据操作会被一直阻塞,
   //           直到有数据被取出腾出空间存放新数据才会解除阻塞,但是程序到目前为止都没有设置数据接收方来取出数据,
   //           所以程序会一直阻塞在这里无法继续执行,这才是报fatal error的本质。

   // 查询管道的长度和容量(由于上面向管道发送了4个数据,故长度为4)
   fmt.Printf("intChan的长度为:%d,容量为:%d \n", len(intChan), cap(intChan)) // intChan的长度为:4,容量为:4

   // 从管道取出数据(重要提醒:按照先进先出的规则)
   int1 := <-intChan // 注:也可以用『int1, ok := <-intChan』的方式从管道取出数据
   int2 := <-intChan
   int3 := <-intChan
   fmt.Printf("int1、int2、int3 分别为 %d%d%d \n", int1, int2, int3) // int1、int2、int3 分别为 1、0、2
   // 重要提醒①:用『int1, ok := <-intChan』的方式取数据,ok表示是否成功取出数据,ok和管道是否已经关闭没有关系。
   //             就算管道已经关闭,但管道里仍有数据未被取出,那么就能成功取出数据,这种情况下ok的值就为true。
   //             如果管道已经关闭,且已经没有数据,此时取出的就不是管道里的正常数据,而是零值,并且ok为false。
   // 重要提醒②:建议统一使用『int1, ok := <-intChan』的方式从管道取出数据,以避免程序出现预期外的错误。
   // 重要提醒③:无论使用哪种方式从管道取出数据,如果管道中没有数据都会发生阻塞,直到有数据写入管道或关闭管道。

   // 查询管道的长度和容量(由于上面向管道发送了4个数据,又从管道取出了3个数据,还剩1个数据,故长度为1)
   fmt.Printf("intChan的长度为:%d,容量为:%d \n", len(intChan), cap(intChan)) // intChan的长度为:1,容量为:4

   // 关闭管道(重要提醒:管道关闭后不能再向其发送数据,否则会报panic,但仍然可以尝试从管道取出数据)
   close(intChan)

   // 下面这行代码是不能执行的,因为上面已经关闭管道,再向管道发送数据就会报panic
   // intChan <- 9527 // panic: send on closed channel

   // 遍历管道
   // 重要提醒①:遍历管道是遍历其当前已写入的数据,和管道容量没有关系。
   // 重要提醒②:遍历管道其实是隐式从管道里取出数据,相当于“value := <-intChan”操作。
   // 重要提醒③:如果管道已经没有数据并且一直不关闭,那么for-range就会一直阻塞。
   // 重要提醒④:当管道被关闭,for-range循环会停止(相当于break效果),但如果管道里仍有数据,将会遍历完剩余数据再停止。
   for value := range intChan {
      fmt.Printf("value = %d \n", value) // value = 4
   }

   // for-range遍历管道将最后一个数据取出,所以长度为0
   fmt.Printf("intChan的长度为:%d,容量为:%d \n", len(intChan), cap(intChan)) // intChan的长度为:0,容量为:4

   // 虽然intChan管道已经关闭了,但仍可以尝试从管道取出数据(不会报错)
   num1 := <-intChan
   fmt.Printf("num1 = %d \n", num1) // num1 = 0

   // 虽然intChan管道已经关闭了,但仍可以尝试从管道取出数据(不会报错)
   num2, ok := <-intChan
   if ok {
      fmt.Printf("成功:num2 = %d \n", num2)
   } else {
      fmt.Printf("失败:num2 = %d \n", num2) // 失败:num2 = 0
   }
}

// ========== 总结 ========== //
// 1、管道的本质就是一个队列,既然是队列那就有先进先出(FIFO,First In First Out)的特点,即先发送给管道的数据会被先取出。
// 2、管道在定义时需要指定数据类型(可以为int、string、map等各种数据类型)和容量,并且后续无法再修改数据类型和容量。
// 3、管道和切片一样,也有长度(len)和容量(cap)两个属性。当管道已经写满数据了还向管道写数据就会发生阻塞,直到有数据被取出;
//    同样地,尝试从没有任何数据的管道取出数据也会发生阻塞,直到有数据被写入。
// 4、管道的发送数据和取出数据都是通过“<-”符号来操作,根据箭头指向可以很直观地看到是向管道写数据还是从管道取数据。
// 5、管道关闭后不能再向其发送数据,但仍然可以尝试从中取数据,且无论管道里是否有数据都不会报错,当最后一个数据被取出后,
//    再从管道里取值就只能取得零值(注:零值指的是数字0、空字符串、false等值)。
// 6、for-range可遍历管道数据,这实际上是隐式从管道取出数据。当管道关闭就会中止循环,此时如果管道里仍有数据,将会遍历完剩余数据再停止。
// 7、管道是线程安全的,多个协程操作同一个管道不会发生资源竞争的问题,这也是管道的主要价值。
// 8、Go提供了函数关闭管道,但是没有提供函数检查管道是否关闭,这不是Go官方的疏忽,而是希望开发者遵循管道只能由数据发送方来关闭的原则,
//    数据接收方只管接收,不需要理会管道是否关闭。

Copyright © 2024 码农人生. All Rights Reserved