go语法 — channel的基础用法

1、channel介绍

Channel 是 go 语言内置的一个非常重要的特性,也是 go 并发编程的两大基石之一(另一个是 go ,也就是 goroutine ),Go内建channel实现了go协程之间数据的读写相关操作。

Go并发哲学是:不通过共享内存来通信,而是通过通信来共享内存。

在 go 语言中,channel 就是 goroutine 之间通过通信来共享内存的手段。 channel 也作为一个队列,会保证数据收发顺序总是按照先入先出的原则进行,可以看作 go 程序内部的一个 FIFO 队列,一些 goroutine 向其中生产数据,另外一些消费数据,同时也会保证同一时刻有且仅有一个goroutine访问channel来收发数据。

2、Channel 的值和类型

跟 slice 、map 这些内置类型一样,channel也是有具体的类型的,channel 只能传递声明的类型的值。
基础类型:双向与单向 channel,可以声明三种类型的 channel:

  • chan T 双向 channel ,既能接收值又能发送值
  • chan<- T send-only channel,发送数据格式:只能往里写(chan是箭头的终点)
  • <-chan T receive-only channel,接收数据格式:只能从里读(chan是箭头的起点) 在函数中使用即:
func foo(ch1 <-chan int)  // 只能从 ch1 里读
func bar(ch2 chan<- int)  // 只能往 ch2 里写

双向 channel chan T 可以被隐式转换成 send-only channel chan<- T 或 receive-only channel <-chan T;而 chan<- T 与 <-chan T 两者则不能互相转换,显式转换也不行

每个 channel 类型的值都会有一个容量(capacity),根据 capacity 大小来区分,可以分为两种:

  • buffered channel:带缓冲的 channel,cap > 0
  • unbuffered channel:不带缓冲的 channel,cap = 0

使用 make 创建 channel:

ch1 := make(chan int, 10)  // buffered channel, cap = 10
ch2 := make(chan int) // unbuffered channel, cap = 0 (make chan 函数第二参数默认值为 0)
var ch3 chan int  // nil 是 chan 的零值(zero value)

注意到上面的例子中还有一种特殊的 channel :nil channel,在只声明但是并未 make 时,channel 的值是零值 nil。nil channel 无论是写入还是读取都会永久阻塞住。

3、channel的基础用法

3.1 向 channel 发送值
ch <- v

需要注意:

  • v 需要和 ch 声明的元素类型相同,例如
  • <- 是 channel-send 即 channel 发送操作符
  • 这里的 ch 不能是 receive-only channel
3.2. 从 channel 里读取结果
<- ch
  • 从 channel 里取数据的操作符叫 channel-receive 操作符,使用它总会有至少一个返回值,它跟 channel-send 操作符长得一样
  • 这里的 ch 不能是 send-only channel
  • channel-receive 操作符大部分时候都返回一个结果,但是也能返回两个结果,第二个结果是用来指示从 channel 里出来的值是否是在 channel 关闭之前读到的,如以下代码所示:
v = <-ch
v, sentBeforeClosed = <-ch  // 先关闭再发送 v,则返回 false

这种方式与操作 map 时的方式类似,被称作 ok-idiom,并且,在 close(ch) 函数执行的时候,会对 ch 发送一条消息,这个动作可以用来通知所有的 goroutine 退出,例如:

package main

func main() {
    done := make(chan struct{})
    c := make(chan int)

    go func() {
        defer close(done)
        
        for {
            x, ok := <-c
            
            if !ok { // close 时会收到一条消息,x 值为 0,ok 为 false
                return
            }

            println(x)
        }
    }()

    c <- 1
    c <- 2
    c <- 3
    close(c)
    <-done // close 时会收到消息,解除阻塞
}
3.3 for-range 操作

使用 for-range 遍历 channel 会比使用 ok-idiom 更简洁,将上面的例子用 for-range 的方式来实现:

func main() {
    done := make(chan struct{})
    c := make(chan int)

    go func() {
        defer close(done)
        
        for x := range c {
            println(x)
        }
    }()

    c <- 1
    c <- 2
    c <- 3
    close(c)
    <-done // close 时会收到消息,解除阻塞
}
3.4 关闭 channel
close(ch)

close 是个 go 内置函数,只能操作 channel 类型,且 close 的对象不能是 receive-only channel。另外,在前面的例子里可以看到, close 发生时,会向被关闭的 channel 发送一条消息,解除阻塞,这个特性可以用来做一些一次性的操作。

3.5 返回 channel 的容量(capacity)
cap(ch)

cap 是 go 的内置函数,会返回一个 int 类型的 channel 容量,可以参考在slice的用法。

3.6 返回 channel buffer 中值的数量
len(ch)

跟 cap 函数类似,len也是 go 语言的内置函数,返回值是已经成功写入 channel buffer 但是还没有从 channel 里读出来的值的数量。在 channel 的操作中,cap 和 len 函数其实用得都不多。

3.7 阻塞场景梳理

针对根据 channel 是否为空和是否关闭,可以分成以下三类来讨论:

  • 空 channel (nil channel)
  • 非空已关闭 channel
  • 非空未关闭 channel
操作 为空 非空已关闭 非空未关闭
close panic panic 成功 close
写入 永久阻塞 panic 成功写入或阻塞
读取 永久阻塞 永不阻塞 成功读取或阻塞

要理解这几种现象,就要看下 channel 的内部结构了,可以认为 channel 内部有三个 FIFO 队列:

  1. 接收数据的 goroutine 队列,是一个无限长的链表,这个队列里的 goroutine 都处于阻塞状态,等待数据从 channel 写入
  2. 发送数据的 goroutine 队列,也是一个无限长的链表,这个队列里的 goroutine 都处于阻塞状态,等待数据向 channel 写入。每个 goroutine 尝试发送的值也和 goroutine 一起存在这个队列里
  3. 值 buffer 队列,是一个环形队列(ringbuffer),它的大小跟 channel 的容量相同。存在这个 buffer 队列里的值跟 channel 元素的类型相同。如果当前 buffer 队列里储存的值的数量达到了 channel 的容量,这个 channel 就「满了」,对于 unbuffered channel 而言,它总是既在「空」状态,又在「满」状态。
4、总结

channel在go中是一种非常有意思而且重要的机制。要想很好的使用channel就必须理解其工作原理。

你可能感兴趣的:(go,golang,开发语言,后端,go)