自学内容网 自学内容网

golang学习笔记26-管道(Channel)【重要】

本节也是GO核心部分,很重要。
注意:Channel更准确的翻译应该是通道,管道实际上叫Pipeline。当然,在GO中,管道专指Channel。
管道本质上是一个队列,队列是数据结构的内容,这里不做赘述。管道对协程的主要作用是提供安全性:因其先进先出的特性,保证了多个协程操作同一个管道时,不会发生资源抢夺问题。
管道的语法是:var 变量名 chan 管道存放的数据类型。管道是引用类型,且和map一样,必须初始化才能写入数据,即make后才能使用。

一、读写数据

管道用<-取(读)数据,存(写)数据,注意,这里的”读“是取出数据,”写“是存入数据,这都会导致管道长度(不是容量)改变!在没有使用协程的情况下,若没有定义管道长度(定义了管道长度的叫缓冲管道),即空管道,这时就取数据,或满管道时放数据,则go都会报错:fatal error: all goroutines are asleep - deadlock!。这里提到了死锁,也是操作系统的概念。
例:

package main

import (
"fmt"
)

func main() {
// 定义一个容量为3的管道作为缓冲,避免阻塞
ch := make(chan int, 3)

// 存入数据
ch <- 1
ch <- 2
ch <- 3
fmt.Printf("存入数据后:长度 = %d, 容量 = %d\n", len(ch), cap(ch))

// 再次存入数据,由于管道已满,这一行会阻塞程序,除非有数据被取出
// ch <- 4 // 取消注释这一行将会导致阻塞,go会报错

// 取出数据
fmt.Printf("取出数据:%d\n", <-ch)
fmt.Printf("取出数据:%d\n", <-ch)
fmt.Printf("取出数据:%d\n", <-ch)
fmt.Printf("取出数据后:长度 = %d, 容量 = %d\n", len(ch), cap(ch))

// 尝试再取数据,管道已空,这会引发阻塞
// 如果取消注释下一行,程序将会在此处阻塞,go会报错
// fmt.Printf("尝试取出额外的数据:%d\n", <-ch)

fmt.Println("程序结束")
}

二、管道的关闭

管道关闭后,就不能向它写数据了,但可以读数据。例:

package main

import (
"fmt"
)

func main() {
// 创建一个容量为3的缓冲管道
ch := make(chan int, 3)

// 向管道中写入数据
ch <- 10
ch <- 20
ch <- 30

// 关闭管道
close(ch)

// 尝试再次写入数据会导致运行时错误:panic: send on closed channel
// ch <- 40 // 取消注释会导致panic,因为管道已关闭

// 读数据,关闭的管道仍然可以读取剩余的数据
fmt.Println("从管道读取数据:", <-ch) // 输出 10
fmt.Println("从管道读取数据:", <-ch) // 输出 20
fmt.Println("从管道读取数据:", <-ch) // 输出 30

// 继续读取,管道已空,读取到的是零值
fmt.Println("尝试读取空管道的数据:", <-ch) // 输出 0,读取的是通道类型的零值

fmt.Println("程序结束")
}

三、管道的遍历

管道由于本质是队列,所以只支持for-range的方式进行遍历,请注意两个细节:
1)对于管道的for-range,只返回value,不返回index
2)在遍历时,如果管道没有关闭,则会出现死锁(deadlock)的错误
3)在遍历时,如果管道已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。

package main

import "fmt"

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

// 向管道中写入数据
for i := 1; i <= 3; i++ {
ch <- i
}

// 1. 如果管道未关闭,会导致 deadlock 错误
// fmt.Println("未关闭管道时遍历:")
// for v := range ch {
//     fmt.Println(v)
// }

// 2. 如果管道关闭,遍历会正常结束
close(ch)
fmt.Println("关闭管道后遍历:")
for v := range ch {
fmt.Println(v)
}

// 若管道关闭后,再次写入数据会报错
// ch <- 4  // 这里会引发 panic: send on closed channel
}

四、协程和管道协同工作

请完成协程和管道协同工作的案例,具体要求:
1)开启一个writeDatat协程,向管道中写入50个整数.
2)开启一个readData协程,从管道中读取writeData写入的数据。
3)注意:writeData和readDate操作的是同一个管道
4)主线程需要等待writeData和readDate协程都完成工作才能退出

package main

import (
"fmt"
"sync"
)

// 向管道中写入数据的协程
func writeData(ch chan int, wg *sync.WaitGroup) {
defer wg.Done() // 协程执行完毕时通知WaitGroup
for i := 1; i <= 50; i++ {
ch <- i
fmt.Printf("写入数据: %d\n", i)
}
close(ch) // 写入完成后关闭管道
}

// 从管道中读取数据的协程
func readData(ch chan int, wg *sync.WaitGroup) {
defer wg.Done() // 协程执行完毕时通知WaitGroup
for data := range ch {
fmt.Printf("读取数据: %d\n", data)
}
}

func main() {
// 创建一个大小为10的管道(缓冲区大小可以根据需求调整)
ch := make(chan int, 10)

// 创建WaitGroup来同步主线程和协程
var wg sync.WaitGroup

// 启动协程,并设置等待数量为2
wg.Add(2)
go writeData(ch, &wg)
go readData(ch, &wg)

// 等待所有协程完成
wg.Wait()

fmt.Println("所有数据写入和读取完成,程序退出")
}

五、管道的声明

默认情况下,管道是可读可写的,但可以声明为只读或只写。

package main

import (
"fmt"
)

func main() {
// 创建一个缓冲管道,避免阻塞
dataChan := make(chan int, 5)

// 声明只写管道
var writeChan chan<- int = dataChan
// 声明只读管道
var readChan <-chan int = dataChan

// 向只写管道写入数据
for i := 1; i <= 5; i++ {
writeChan <- i
fmt.Printf("写入数据: %d\n", i)
}
close(writeChan) // 关闭写入管道

// 从只读管道读取数据
for value := range readChan {
fmt.Printf("读取数据: %d\n", value)
}
}

六、select

这个select可不是数据库语言,这是用于解决多个管道的选择问题的,select操作也可以叫做多路复用,可以从多个管道中随机公平地选择一个来执行。注意,这不是switch,switch是顺序选择,这里是随机选择。一些细节:
1.case后面必须进行的是io操作,即case c := <-chan1:,不能是等值,即case c:
2.default防止select被阻塞,加入default

package main

import (
"fmt"
"time"
)

func main() {
chan1 := make(chan int) // 有了select,即便无缓冲也不会阻塞
chan2 := make(chan string)
go func() {
time.Sleep(time.Second * 1)
chan1 <- 1
}()
go func() {
time.Sleep(time.Second * 2)
chan2 <- "hello"
}()
select {
case v := <-chan1:
fmt.Println("intchan:", v)
case v := <-chan2:
fmt.Println("stringchan:", v)
default:
fmt.Println("防止阻塞")
}
}

上述代码其实不完善,因为无论select之前怎么改,程序都只输出”防止阻塞“,若要执行case,就需要for循环来持续监听管道

package main

import (
"fmt"
"time"
)

func main() {
// 创建一个缓冲通道
chan1 := make(chan int, 1)
chan2 := make(chan string, 1)

// 启动 goroutine 向 chan1 写入数据
go func() {
time.Sleep(time.Second * 1)
chan1 <- 1
}()

// 启动 goroutine 向 chan2 写入数据
go func() {
time.Sleep(time.Second * 2)
chan2 <- "hello"
}()

// 持续监听通道
for {
select {
case v := <-chan1:
fmt.Println("intchan:", v) // 如果 chan1 被写入,打印数据
return                     // 读取后退出循环
case v := <-chan2:
fmt.Println("stringchan:", v) // 如果 chan2 被写入,打印数据
return                        // 读取后退出循环
default:
fmt.Println("防止阻塞") // 如果没有通道可读,打印该信息
// 等待一段时间,防止立即进入下一循环而输出过多信息
time.Sleep(500 * time.Millisecond)
}
}
}

多次运行你会发现,总是输出第一个协程的信息,但这不违背select随机选取的原则,因为select选取的仅是准备好的通道。由于第二个协程比第一个协程慢1秒,所以总是第一个先准备好。所以想要随机输出协程信息,睡眠时间都改为一样即可,比如1秒,读者可自行尝试,多次运行,结果一定会不同。


原文地址:https://blog.csdn.net/weixin_54259326/article/details/142611637

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!