golang学习笔记26-管道(Channel)【重要】
本节也是GO核心部分,很重要。
注意:Channel更准确的翻译应该是通道,管道实际上叫Pipeline。当然,在GO中,管道专指Channel。
管道本质上是一个队列,队列是数据结构的内容,这里不做赘述。管道对协程的主要作用是提供安全性:因其先进先出的特性,保证了多个协程操作同一个管道时,不会发生资源抢夺问题。
管道的语法是:var 变量名 chan 管道存放的数据类型
。管道是引用类型,且和map一样,必须初始化才能写入数据,即make后才能使用。
目录
- 一、读写数据
- 二、管道的关闭
- 三、管道的遍历
- 四、协程和管道协同工作
- 五、管道的声明
- 六、select
一、读写数据
管道用<-
取(读)数据,存(写)数据,注意,这里的”读“是取出数据,”写“是存入数据,这都会导致管道长度(不是容量)改变!在没有使用协程的情况下,若没有定义管道长度(定义了管道长度的叫缓冲管道),即空管道,这时就取数据,或满管道时放数据,则go都会报错:fatal error: all goroutines are asleep - deadlock!
。这里提到了死锁,也是操作系统的概念。
例:
package mainimport ("fmt"
)func main() {// 定义一个容量为3的管道作为缓冲,避免阻塞ch := make(chan int, 3)// 存入数据ch <- 1ch <- 2ch <- 3fmt.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 mainimport ("fmt"
)func main() {// 创建一个容量为3的缓冲管道ch := make(chan int, 3)// 向管道中写入数据ch <- 10ch <- 20ch <- 30// 关闭管道close(ch)// 尝试再次写入数据会导致运行时错误:panic: send on closed channel// ch <- 40 // 取消注释会导致panic,因为管道已关闭// 读数据,关闭的管道仍然可以读取剩余的数据fmt.Println("从管道读取数据:", <-ch) // 输出 10fmt.Println("从管道读取数据:", <-ch) // 输出 20fmt.Println("从管道读取数据:", <-ch) // 输出 30// 继续读取,管道已空,读取到的是零值fmt.Println("尝试读取空管道的数据:", <-ch) // 输出 0,读取的是通道类型的零值fmt.Println("程序结束")
}
三、管道的遍历
管道由于本质是队列,所以只支持for-range的方式进行遍历,请注意两个细节:
1)对于管道的for-range,只返回value,不返回index
2)在遍历时,如果管道没有关闭,则会出现死锁(deadlock)的错误
3)在遍历时,如果管道已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
package mainimport "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 mainimport ("fmt""sync"
)// 向管道中写入数据的协程
func writeData(ch chan int, wg *sync.WaitGroup) {defer wg.Done() // 协程执行完毕时通知WaitGroupfor i := 1; i <= 50; i++ {ch <- ifmt.Printf("写入数据: %d\n", i)}close(ch) // 写入完成后关闭管道
}// 从管道中读取数据的协程
func readData(ch chan int, wg *sync.WaitGroup) {defer wg.Done() // 协程执行完毕时通知WaitGroupfor data := range ch {fmt.Printf("读取数据: %d\n", data)}
}func main() {// 创建一个大小为10的管道(缓冲区大小可以根据需求调整)ch := make(chan int, 10)// 创建WaitGroup来同步主线程和协程var wg sync.WaitGroup// 启动协程,并设置等待数量为2wg.Add(2)go writeData(ch, &wg)go readData(ch, &wg)// 等待所有协程完成wg.Wait()fmt.Println("所有数据写入和读取完成,程序退出")
}
五、管道的声明
默认情况下,管道是可读可写的,但可以声明为只读或只写。
package mainimport ("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 <- ifmt.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 mainimport ("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 mainimport ("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秒,读者可自行尝试,多次运行,结果一定会不同。