August Rush

一个还在努力成长的小火汁!

游龙当归海,海不迎我自来也。

We create our own demons.

You can reach me at augustrush0923@gmail.com
Go语言基础-管道Channel
发布:2024年01月21日 | 作者:augustrush | 阅读量: 422

Golang中的管道(channel)

管道(channel)用来在goroutine中进行数据通信,它遵循队列数据结构,有先进先出的特性。管道是引用类型

管道的定义

// var 管道名称 chan 管道类型
var numCh chan int
var strCh chan string
var dictCh chan map[string]string

管道的初始化

声明后还不能使用,需要对管道进行初始化。可以通过make函数来进行初始化。

// 管道引用 = make(管道类型, 管道容量)
var ch chan int

ch = make(chan int)

var strCh chan string

strCh = make(chan string, 10)

也可以通过海象运算符进行快捷初始化:

ch := make(chan int)

管道的操作

管道只有三种操作:

  • 发送数据
  • 接收数据
  • 关闭管道

发送和接收数据都通过操作符<-来进行。

假设我们有一个管道:

// 创建一个容量为3的int类型管道
ch := make(chan int, 3)

发送

发送数据时,<-操作符在管道的后面

// 往管道ch中写入数据10
ch <- 10

接收

接收数据时,<-操作符在管道的前面

// 从管道ch中读取数据
recive := <-ch

关闭

通过close函数来关闭管道

close(ch)

关闭管道需要注意一些事情:

只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭管道。管道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭管道不是必须的(程序结束后没关闭的管道会被垃圾回收机制自动清理掉)。

关闭后的管道有以下特点:

  1. 对一个关闭的管道再发送数据就会导致panic: send on closed channel
  2. 对一个关闭的管道进行接收会一直获取值,当管道没有数据时会接收管道类型对应的零值。
  3. 关闭一个已经关闭的管道会导致panic: close of closed channel

管道的阻塞

无缓冲的管道

如果创建管道的时候没有指定容量,那么这个管道为无缓冲的管道,无缓冲的管道又称为阻塞的管道。

func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Println("send successful!")
}

这段代码能够通过编译,但是在执行的时候会出现以下错误:

$ go run main.go
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        /Users/augustrush/Documents/awesomeProject/main.go:12 +0x34

deadlock表示我们程序中的goroutine都被挂起导致程序死锁了。为什么会出现deadlock错误呢?

因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。就像田径比赛中的4x100接力赛,想要完成交棒必须有一个能够接棒的运动员,否则只能等待。简单来说就是无缓冲的通道必须有至少一个接收方才能发送成功。`

上面的代码会阻塞在ch <- 10这一行代码形成死锁.

有缓冲的管道

还有另外一种解决上面死锁问题的方法,那就是使用有缓冲区的通道。我们可以在使用make函数初始化通道时,可以为其指定通道的容量,例如:

func main() {
    ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}

只要通道的容量大于零,那么该通道就属于有缓冲的通道,通道的容量表示通道中最大能存放的元素数量。当通道内已有元素数达到最大容量后,再向通道执行发送操作就会阻塞,除非有从通道执行接收操作。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

循环遍历管道

有两种方式来对管道进行遍历:

  • for循环
  • for range

for循环

通过for循环可以遍历循环管道:

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

    for i := 0; i < 10; i++ {
        // 循环加入10条数据
        ch <- i
    }

    for i := 0; i < 10; i++{
        // 从管道内取出数据并打印输出
        fmt.Println(<-ch)
    }
}

通过这种方式不用在意管道是否关闭,即使管道关闭后,根据管道特性也可以取出管道类型对应的零值。

for range

通过for range可以遍历循环管道,但需注意,如果管道未关闭,通过for range会造成deadlock

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

    for i := 0; i < 10; i++ {
        // 循环加入10条数据
        ch <- i
    }

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

输出结果:

$ go run main.go
0
1
2
3
4
5
6
7
8
9
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        /Users/augustrush/Documents/awesomeProject/main.go:16 +0xac

管道与Goroutine协作案例

开启两个协程,同时往同一管道执行发送与接收操作:

func main() {
    // 定义管道
    dataCh := make(chan int, 10)

    wg.Add(1)
    // 创建一个协程执行发送操作
    go func() {
        defer wg.Done()
        for i := 0; i < 10; i++ {
            dataCh <- i
            fmt.Printf("【发送】数据%v已发送\n", i)
        }
        // 关闭管道,不然for range会造成死锁
        close(dataCh)
    }()

    wg.Add(1)
    // 创建一个协程执行接收操作
    go func() {
        defer wg.Done()
        for v := range dataCh {
            fmt.Printf("【接收】数据%v已接收\n", v)
        }
    }()

    wg.Wait()
    fmt.Println("退出...")
}

单向管道

Golang中的管道可以进行读也可以进行写。但在有些场景下,只会对管道进行读操作。另一些场景下,只会对管道进行写操作。

这时,Golang就贴心的给出了两种特殊类型管道:

  • 只读管道:只能接收不能发送
  • 只写管道:只能发送不能接收

在默认情况下,声明的管道都是双向管道。即可读可写

// var ch chan int // 双向管道,可读可写
func main() {
    ch := make(chan int, 10)
    ch <- 1
    ch <- 2
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

只读管道

声明一个只读管道,只需要在chan关键字前面加上操作符<-

var ch <-chan int // 只读管道:只读不写

只读管道只能发送,当进行写操作时会报错

// 
func main() {
    ch := make(chan int, 10)
    for i := 0; i < 10; i++ {
        ch <- i
    }
    var readOnlyCh <-chan int = ch
    fmt.Println(<-readOnlyCh)
    fmt.Println(<-readOnlyCh)
    // readOnlyCh <- 11 
    // invalid operation: cannot send to receive-only channel readOnlyCh (variable of type <-chan int)
}

只写管道

声明一个只写管道,只需要在chan关键字后面加上操作符<-

var ch chan <- int  // 只写管道:只写不读

只写管道只能接收,当进行读操作时会报错

// 
func main() {
    onlyWriteCh := make(chan<- int, 10)
    onlyWriteCh <- 1
    onlyWriteCh <- 2

    // fmt.Println(<-onlyWriteCh)
    // invalid operation: cannot receive from send-only channel onlyWriteCh (variable of type chan<- int)
}

多路复用

在某些场景下我们需要同时从多个通道接收数据,这个时候就可以用到golang给我们提供的select多路复用。

select语句具有以下特点。

  • 可处理一个或多个channel的发送/接收操作。
  • 如果多个case同时满足,select会随机选择一个执行。
  • 对于没有caseselect会一直阻塞,可用于阻塞main函数,防止退出。

select的使用方式类似于switch语句,它也有一系列case分支和一个默认的分支。每个case分支会对应一个通道的通信(接收或发送)过程。select会一直等待,直到其中的某个case的通信操作完成时,就会执行该case分支对应的语句。具体格式如下:

select {
case <-ch1:
    //...
case data := <-ch2:
    //...
case ch3 <- 10:
    //...
default:
    //默认操作
}

例如:

func main() {
    intChan := make(chan int, 10)
    for i := 1; i <= 10; i++ {
        intChan <- i
    }
    strChan := make(chan string, 10)
    for i := 1; i <= 10; i++ {
        strChan <- fmt.Sprintf("channel-%d", i)
    }

    for {
        select {
        case i := <-intChan:
            fmt.Printf("从intChan获取数据:%d\n", i)
        case s := <-strChan:
            fmt.Printf("从strChan获取数据:%s\n", s)
        default:
            fmt.Println("读取数据完毕!")
            return
        }
    }
}

上述代码运行结果如下:

$ go run main.go
从intChan获取数据:1
从strChan获取数据:channel-1
从intChan获取数据:2
从strChan获取数据:channel-2
从intChan获取数据:3
从strChan获取数据:channel-3
从intChan获取数据:4
从strChan获取数据:channel-4
从strChan获取数据:channel-5
从strChan获取数据:channel-6
从strChan获取数据:channel-7
从strChan获取数据:channel-8
从strChan获取数据:channel-9
从intChan获取数据:5
从strChan获取数据:channel-10
从intChan获取数据:6
从intChan获取数据:7
从intChan获取数据:8
从intChan获取数据:9
从intChan获取数据:10
读取数据完毕!

使用select时需要注意以下几点:

  • select一般搭配for循环来进行操作。
  • 使用select多路复用来获取管道里面的数据的时候,管道不能关闭,否则根据管道的特性(对一个关闭的管道取值会得到该管道类型对应的零值)会一直死循环。
  • 要注意在default中进行退出,否则会一直在default中死循环。
  • 不能使用break跳出for循环,需要使用return来结束select


  • 标签云

  • 支付宝扫码支持一下

  • 微信扫码支持一下



基于Nginx+Supervisord+uWSGI+Django1.11.1+Python3.6.5构建

京ICP备20007446号-1 & 豫公网安备 41100202000460号

网站地图 & RSS | Feed