一、channel(管道) 的基本介绍。

1、channle 本质就是一个数据结构-队列,数据是先进先出【FIFO : first in first out】。

2、channel是线程安全的,多个 goroutine 访问时,不需要加锁。

3、channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。

4、channle 的数据放满后,就不能再放入了,如果从 channel 取出数据后,可以继续放入。

5、 主线程和其他协程读取管道数据不一样的地方:主线程在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock。如果其他协程读取数据会阻塞等待数据输入。所以主线程读取管道数据,如果 channel 数据取完了,再取,这时候管道在其他协程有关闭行为才能保证不死锁。

6、channel(管道)dead lock(死锁)的错误发生的原因有2点,1、一直往管道写数据,但是没有读数据的地方。2、一直读数据,但是没写数据的地方。这个和是主线程还是协程没有关系。

二、定义/声明 channel 的语法:

var 变量名 chan 数据类型

示例:

var intDatas chan int //intDatas 用于存放 int 数据
var mapDatas chan map[int]string //mapDatas 用于存放 map[int]string 类型
var stus chan Student  //用于存放Student
var pstus chan *Student  //用于存放Student的指针

说明:

1、 channel 是引用类型, channel 必须初始化才能写入数据, 即 make 后才能使用。

2、管道是有类型的。var intDatas chan int 管道变量intDatas只能写入int类型数据。管道类型是空接口 var testChan chan interface{},可以存放任意类型。

1.1.1. 三、向管道写数据,读数据。

1、管道是引用类型。

    // 创建一个可以存放 3 个 int 类型的管道
    var intChan chan int = make(chan int,3)

    //intChan 的值=0xc000092080 , intChan的地址=0xc000088018
    fmt.Printf("intChan 的值=%v , intChan的地址=%p \n", intChan, &intChan)

可以看到管道的值是一个地址,管道是引用类型。

2、向管道写数据。

    var intChan chan int = make(chan int,3)

    //向管道写入3个数据。当我们给管写入数据时,不能超过其容量,否则报错死锁deadlock。
    intChan<- 10
    num := 211
    intChan<- num
    intChan<- 50

    //看看管道的长度和 cap(容量)
    fmt.Printf("管道 len= %v cap=%v \n", len(intChan), cap(intChan))// 管道 len= 3 cap=3

说明:

1、向管道写数据,不能超过管道的容量。例如:上面管道的容量是3,写入4个数据,运行就会报错死锁deadlock。除非有一个协程在读数据。

2、len(intChan) 获取管道的长度,也就是管道中现在存了多少个数据。

3、cap(intChan) 获取管道的容量。make给管道分配空间时,分配的容量。

3、读取管道的数据。

    var intChan chan int = make(chan int,3)
    //向管道写入3个数据。
    intChan<- 10
    intChan<- 211
    intChan<- 50

    //从管道中读取数据
    num2 := <-intChan
    fmt.Println("num2=", num2) //num2= 10

    //channel len= 2 cap=3
    fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan))

说明:

1、在没有使用协程的情况下,如果管道数据已经全部取出,再取就会报错deadlock。

例如:管道中有3个数据,读取4次就会报错死锁deadlock。

2、可以把管道的数据取出,但是不用变量接收。<-intChan,把这个数据丢掉。

4、空接口的管道。存放任意类型数据。

type Student struct {
    Name string
    Age int
}
func main() {
    allChan := make(chan interface{},3)
    allChan<- Student{Name:"张三",Age:28}
    allChan<- 10
    allChan<- "你好"

    //从管道中读取数据
    stuInterface := <-allChan
    //类型断言,返回的ok是bool类型,表示是否成功。
    stu,ok := stuInterface.(Student)
    if ok {
        fmt.Println(stu.Name)
    }else {
        fmt.Println("不是Student类型...")
    }
}

使用类型断言stu,ok := stuInterface.(Student),判断从管道取出的数据类型。

1.1.2. 四、channel 的遍历和关闭。

1、关闭channel。

使用内置函数 close 可以关闭 channel。当 channel 关闭后,就不能再向 channel 写数据了,但是仍然可以从该 channel 读取数据。

    intChan := make(chan int,3)
    intChan<- 10
    intChan<- 20

    // 关闭管道
    close(intChan)

    //intChan<- 30 //关闭管道之后再写数据,运行报错 panic: send on closed channel

    // 读数据
    num := <-intChan
    fmt.Println("num =",num) //num = 10

关闭管道之后再写数据,运行会报错 panic: send on closed channel,但是可以读数据。

2、channel 的遍历。

channel 支持 for--range 的方式进行遍历,请注意两个细节:

  • 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误。
  • 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
    nameChan := make(chan string,3)
    nameChan<- "张三"
    nameChan<- "李四"
    nameChan<- "王二"

    // 关闭管道后,才能遍历
    close(nameChan)

    // 遍历输出
    for v := range nameChan{
        fmt.Println(v)
    }

必须关闭管道之后,才能遍历读数据。否则报错。

1.1.3. 五、管道数据的读取要点(防止死锁)。

1、主线程中读取。

    nameChan := make(chan string,3)
    close(nameChan) // 关闭管道后,可以任意读取。

    for v := range nameChan{
        fmt.Println(v)
    }

    // 读取数据,使用标识ok,判断是不是读到了数据。
    num,ok:= <-nameChan

    if ok {
        fmt.Println("读取数据成功,num=",num)
    }else {
        fmt.Println("读取数据失败") //读取数据失败
    }

上面代码中,读取管道数据:

1、close(nameChan) 关闭管道后,可以任意读取管道的数据。即使没有数据,也可以再读。不会死锁。

2、如果管道不关闭,不能使用for--range 的方式进行遍历(因为会一直循环读取数据,如果其他地方没有关闭管道,会发生死锁);可以使用普通方式读取,但是管道没有数据,再继续读,就会报错死锁。

3、管道关闭之后,使用 num,ok := <-nameChan读取数据;然后使用标识ok,判断是不是读到了数据,没读到数据表示到管道末尾了。

2、协程(goroutine)中读取管道(channel)数据。

在协程中读取channel数据,无论channel是否关闭,都不会出错。

  • 如果channel是开启的:使用for--range 的方式式进行遍历会一直阻塞,直到管道关闭,读取完所有的数据。使用或普通方式读取,如果有数据就读取到数据,如果没数据也会一直阻塞,直到进入新的数据完成这一次读取或者管道关闭。
  • 如果channel关闭:无论使用for--range 的方式还是普通方式,直接读取。不会出错。
    //管道
    nameChan := make(chan string,3)
    //协程
    go func() {
        nameChan<- "张三"
        for v := range nameChan{
            fmt.Println("读取数据成功,name=",v)
        }
        fmt.Println("协程 end......")
    }()

    for {
        time.Sleep(time.Second * 3)
        fmt.Println("first sleep ......")
        break
    }
    nameChan<- "222"
    nameChan<- "3333"
    nameChan<- "4444"
    close(nameChan) // 关闭管道
    for {
        time.Sleep(time.Second * 3)
        break
    }
    fmt.Println("主线程end。。。。")

输出如下:

读取数据成功,name= 张三

first sleep ......

读取数据成功,name= 222

读取数据成功,name= 3333

读取数据成功,name= 4444

协程 end......

主线程end。。。。

3、具体有没有死锁出现。还得看具体问题。

channel(管道)死锁发生的原因有2点,1、一直往管道写数据,但是没有读数据的地方。2、一直读数据,但是没写数据的地方。

这个和是主线程还是协程没有关系。

func main() {
    //管道
    nameChan := make(chan string,3)

    // 协程
    go func() {
        time.Sleep(time.Second * 3)
        nameChan<- "aaa"
        close(nameChan)
    }()

    // 循环
    for {
        fmt.Println("in for loop....")
        name,ok := <-nameChan
        if ok {
            fmt.Println("name==",name)
            break
        }else {
            fmt.Println("没有读到。。。。")
        }
    }

}

运行结果如下:

in for loop....
name== aaa

总结:主线程的读取操作 name,ok := <-nameChan 阻塞等待协程对nameChan管道的操作。如果协程中没有对nameChan管道的操作,就会报死锁错误。因为主线程一直读取,没有其他地方写入或者关闭管道。

results matching ""

    No results matching ""