Go语言学习之Go协程:信道

前言

goroutine是go语言程序的并发执行的基本单元,多个goroutine的通信是需要依赖channel,叫做信道

1.信道的定义与使用

每个信道只能传递一种数据类型的数据,所以声明信道的时候,需要指定数据类型(string、int等)

信道实例 := make(chan 信道类型)

假如我要创建一个可以传递int数据类型的信道

//定义信道
pipline := make(chan int)

信道的数据操作,无非就是两种:发送数据和读取数据

// 往信道中发送数据
pipline<- 200

// 从信道中取出数据,并赋值给mydata
mydata := <-pipline

信道用完了,可以对其进行关闭,避免有人一直在等待

close(pipline)

对于一个已经关闭的信道 如果再去关闭是会报错的,所以我们需要判断一下 信道 是不是被关闭

当从信道读取数据时,可以有多个返回值,其中第二个可以表示信道是否被关闭,如果已经被关闭就返回false,如果还没被关闭返回true

x, ok := <-pipline

2.信道的容量和长度

一般创建信道都是使用make函数,make函数接收两个参数

第一个参数: 必填 必定信道类型

第二个参数: 选填 不填默认为0,指定信道的容量(可以缓存多少数据)

对于信道的容量很重要:

  • 当容量为0时,说明信道中不能存放数据,在发送数据时,必须要求立马有人接收,否则会报错。此时的信道称之为无缓冲信道
  • 当容量为1时,说明信道中只能缓存一个数据,如果信道中已经有一个数据,此时再往里发送数据,会造成阻塞;可以利用信道这点做锁
  • 当容量大于1时,信道中可以存放多个数据,可以用于多个协程间的通信管道,共享资源

我们现在知道 信道就是一个容器

如果将它比作一个纸箱子

  • 它可以装10本书,代表其容量为10
  • 当前只装了1本书,代表其当前长度为1

信道的容量可以用cap()函数获取,信道的长度可以用len()函数来获取

package main

import "fmt"

func main() {
    pipline := make(chan int, 10)
    fmt.Printf("信道可缓冲 %d 个数据\n", cap(pipline))
    pipline<- 1
    fmt.Printf("信道中当前有 %d 个数据", len(pipline))
}

输出如下

信道可缓冲 10 个数据
信道中当前有 1 个数据

3.缓冲信道无缓冲信道

按照是否可缓冲数据可分为:缓冲信道无缓冲信道

缓冲信道

允许信道里存储一个或多个数据,这意味着,设置了缓冲区后,发送端和接收端可以处于异步的状态

pipline := make(chan int, 10)

无缓冲信道

在信道里无法存储数据,这意味着,接收端必须先于发送端准备好,以确保你发送完数据和,有人立马接收数据,否则发送端就会造成阻塞

原因很简单,信道中无法存储数据;也就是说发送端和接收端是同步运行的

pipline := make(chan int)

// 或者
pipline := make(chan int, 0)

4.双向通道和单向通道

通常情况下,我们定义的都是双向信道,可以发送数据,也可以接收数据

但有时候,我们希望对信道的数据流做一些控制,比如这个信道只能接收数据或者这个信道只能发送数据

因此就有了双向信道单向信道两种分类

4.1双向信道

默认情况下你定义的信道都是双向的,比如下面代码

import (
    "fmt"
    "time"
)

func main() {
    pipline := make(chan int)

    go func() {
        fmt.Println("准备发送数据: 100")
        pipline <- 100
    }()

    go func() {
        num := <-pipline
        fmt.Printf("接收到的数据是: %d", num)
    }()
    // 主函数sleep,使得上面两个goroutine有机会执行
    time.Sleep(1)
}

4.2单向信道

可以细分为只读信道只写信道

定义只度信道

var pipline = make(chan int)
type Receiver = <-chan int // 关键代码:定义别名类型
var receiver Receiver = pipline

定义只写信道

var pipline = make(chan int)
type Sender = chan<- int  // 关键代码:定义别名类型
var sender Sender = pipline

仔细观察区别在于<-符号在关键字chan的左边还是右边

  • <-chan 表示这个信道,只能从里发出数据,对于程序来说就是只读

  • chan<- 表示这个信道,只能从外面接收数据,对于程序来说就是只写

为什么要先定义一个双向信道,在定义一个单向信道呢,比如这样写

import (
    "fmt"
    "time"
)
 //定义只写信道类型
type Sender = chan<- int  

//定义只读信道类型
type Receiver = <-chan int 

func main() {
    var pipline = make(chan int)

    go func() {
        var sender Sender = pipline
        fmt.Println("准备发送数据: 100")
        sender <- 100
    }()

    go func() {
        var receiver Receiver = pipline
        num := <-receiver
        fmt.Printf("接收到的数据是: %d", num)
    }()
    // 主函数sleep,使得上面两个goroutine有机会执行
    time.Sleep(1)
}

信道本身就是为了传输数据而存在的,如果只有接收者或者只有发送者,那信道就变成了只入不出或者只出不入,那就没意义了.

5.遍历信道

遍历信道,可以搭配使用for搭配range关键字,在range时,要确保信道处于关闭状态,否则循环会阻塞

import "fmt"

func fibonacci(mychan chan int) {
    n := cap(mychan)
    x, y := 1, 1
    for i := 0; i < n; i++ {
        mychan <- x
        x, y = y, x+y
    }
    // 记得 close 信道
    // 不然主函数中遍历完并不会结束,而是会阻塞。
    close(mychan)
}

func main() {
    pipline := make(chan int, 10)

    go fibonacci(pipline)

    for k := range pipline {
        fmt.Println(k)
    }
}

当容量为1时,说明信道只能缓存一个数据,若信道中已有一个数据,此时再往里发送数据,会造成程序阻塞。 利用这点可以利用信道来做锁