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秒,读者可自行尝试,多次运行,结果一定会不同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术卷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值