跳至主要內容

管道

LincZero大约 3 分钟

管道

(先读协程内容先)

管道 (channel)

概括

特性介绍:

  • 本质

    • 本质是一个数据结构:队列 (先进先出)

    • 有类型,例如一个string只能存放string类型数据

    • 自身线程安全,多协程访问时无需加锁,本身线程安全。与协程关系密切

  • 用处

    • 可作为流式IO。类似 C++ 的std::cout流那种感觉
    • 线程安全,故可和协程配合使用

声明定义、使用

// 语法:如下。引用类型,必须初始化或make后才能写入数据
var 变量名 chan 数据类型

// 示例
package main
import(
	"fmt"
)

func main() {
    // 声明定义
    var intChan chan int
    intChan = make(chan int, 3)			// 存放3个int类型的数据
    fmt.Printf("intChan %v\n", intChane)	// 打印地址,表示是引用类型
    
    // 写
    intChan <- 10
    num := 20
    int chan <- num
    fmt.Printf("intChan 长%v 容量%v\n", len(intChan), Cap(intChan))	// 2 3
    
    // 读 (先进先出的取)
    num1 := <- intChan	// 10
    num2 := <- intChan	// 20
}

方法

关闭管道

关闭管道后,管道只读不可写

// 原型
func close(c chan<- Type)

// 例子
close(intChan)

遍历

管道遍历需要注意:遍历前必须先关闭管道,否则便利到最后时会出现 deadlock 错误

(猜测:关闭管道的原理应该是往管道符的末尾添加一个类似文件结尾 (EOF) 之类的标志,让遍历能自动结束)

package main
import(
	"fmt"
)

func main() {
    var intChan chan int				// 管道
    intChan = make(chan int, 100)
    
    // 遍历写
    for i:=0; i<100; i++ {
        intChan <- i
    }
    
    // 关闭管道。遍历前必须先关闭管道,否则便利到最后时会出现 deadlock 错误
    close(intChan)
    
    // 遍历读 (for range 版)
    for v := range intChan {
        fmt.Println(v)
    }
}

扩展

(实战) 协程和管道 协同工作

略。正常写就行,注意一下关闭管道和协程计数器的问题就行

有BUG:话说他这案例没有解决读结束时还没写完及关闭管道的问题啊

package main
import(
	"fmt"
    "time"
    "sync"
)

var wg sync.WaitGroup	// 协程计数器

// 写
func writeData(intChan chan int) {
    defer wg.Done()
    for i:=1; i<=20; i++{
        intChan <- i
        fmt.Println("写入", i)
        time.Sleep(time.Second)
    }
    
    // 管道关闭
    close(intChan)
}

// 读
func readData(intChan chan int) {
    defer wg.Done()
    for v := range intChan {
        fmt.Println("读取", v)
        time.Sleep(time.Second)
    }
}

func main() {
    intChan := make(chan int, 10)		// 管道(注意这里管道容量10比循环次数20要小,但由于边写边读没问题,若不读则会发生阻塞)
    
    wg.Add(2)
    go writeData(intChan)
    go readData(intChan)
    wg.wait()
}

只读/只写管道

管道可以声明成只读或只写的(应该是作为形参类型使用的,配合协程函数一个读一个写)

下面这个案例不好,用法没什么意义

package main
import(
	"fmt"
)

func main() {
    // 可读可写 (默认)
    var intChan1 chan int
    
    // 只写
	var intChan2 chan<- int
    intChna2 = make(chan int, 3)
    
    // 只读
    var intChan3 <-chan int
}

管道的阻塞

这章有BUG,教程太拉了。命名是Add(2)导致死锁的原因,不关阻塞的事)

当管道只写不读,管道 (缓冲区) 可以会满,导致阻塞。 但是 写快读慢 则不会出现阻塞问题。(???,会吧,垃圾教程)

select case 语法

select功能:

  • 解决多个管道的选择问题,也叫多路复用。可以从多个管道中随机公平地选择一个执行
  • 也可以解决阻塞问题

注意项:

  • case后面必须是io操作,不能是等值,随机去选择一个io操作
  • default可防止select被阻塞住,加入default
package main
import (
	"fmt"
)

func main() {
    // int管道
    intChan := make(chan int, 1)
    go func() {
        time.Sleep(time.Second*3)
        intChan <- 10
    }()
    
    // string管道
    stringChan := make(chan string, 1)
    go func(){
        time.Sleep(time.Second*2)
        stringChan <- "msbgolang"
    }
    
    // fmt.Println(<-intChan) // 取数据本身就阻塞
    
    select{
        case v := <-intChan:
        	fmt.Println("intChan:", v)
        case v := <-stringChan:
	        fmt.Println("intChan:", v)
        default:
	        fmt.Println("防止阻塞")
    }
}

defer + recover 机制处理错误

背景:多个协程工作,避免 其中一个协程出现panic 导致程序崩溃

案例:就正常写。协程函数里加个 defer func(){ err:= recover; if err != nil {...}} 就行了