1、channel的作用

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。

为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。

channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。

通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。

每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

2、channel类型

声明通道类型的格式如下:

var 变量 chan 元素类型

示例:

// 声明 int 类型的通道
var ch1 chan int

// 声明 string 类型的通道
var ch2 chan string

// 声明 string切片 类型的通道
var ch3 chan []string

3、创建channel

var ch chan int
// 通道类型的空值是nil(未初始化,故为nil)
fmt.Println(ch)
fmt.Printf("ch 的地址:%p\n", ch)

// 声明的通道后需要使用make函数初始化之后才能使用。
ch1 := make(chan int, 1)
fmt.Println(ch1)
fmt.Printf("ch1 的地址:%p\n", ch1)

创建channel的格式如下:

make(chan 元素类型, [缓冲大小])

channel的缓冲大小是可选的。

示例:

ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

4、channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用<-符号。

ch2 := make(chan int, 2)

// 发送数据到通道
ch2 <- 10
ch2 <- 20

// 接收 来自通道的 消息
// 使用变量接收
x := <-ch2
fmt.Println(x)
// 直接接收,忽略结果
fmt.Println(<-ch2)

// 关闭 通道
close(ch2)

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。

通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的

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

1.对一个关闭的通道再发送值就会导致panic。
2.对一个关闭的通道进行接收会一直获取值直到通道为空。
3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
4.关闭一个已经关闭的通道会导致panic。

5、无缓冲通道

(错误示例)代码:

func main(){
    // 无缓冲通道
    ch := make(chan int)
    ch<-10
    // 即使后面立即获取通道值也没用,前面已经阻塞了
    fmt.Println(<-ch)
}

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

为什么会出现deadlock错误呢?

因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。

就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。

上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?

一种方法是启用一个goroutine去接收值,例如:

func main(){
    ch := make(chan int)
    go func(r chan int) {
        rc := <-r
        fmt.Println("接收到的值是:", rc)
    }(ch)

    ch<-10

    time.Sleep(time.Second)
}

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。

相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道

6、有缓冲通道

在使用make函数初始化通道的时候为其指定通道的容量

代码示例:

func main(){
    ch := make(chan int, 1)
    ch<-10
    fmt.Println("发送成功")
    x := <-ch
    fmt.Println("获取成功,x = ", x)
}

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。

就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

注意:
可以使用内置的 len 函数获取通道内元素的数量,使用 cap 函数获取通道的容量。(一般很少这样做)

7、close()

可以通过内置的close()函数关闭channel(如果你的管道不往里存值或者取值的时候一定记得关闭管道)

func main(){
    // close()
    ch := make(chan int)
    go func() {
        for i:=0;i<10;i++{
            ch<-i
        }
        close(ch)
        fmt.Println("关闭成功")
    }()
    for{
        if data,ok := <-ch; ok {
            fmt.Println(data)
        }else{
            break
        }
    }
}

如何优雅的从通道循环取值

当通过通道发送有限的数据时,我们可以通过close函数关闭通道来告知从该通道接收值的goroutine停止等待。

当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。

那如何判断一个通道是否被关闭了呢?

可以看下面的示例:

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    // 开启goroutine将0~100的数发送到ch1中
    go func() {
        for i := 0; i < 100; i++ {
            ch1 <- i
        }
        close(ch1)
    }()
    // 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
    go func() {
        for {
            i, ok := <-ch1 // 通道关闭后再取值ok=false
            if !ok {
                break
            }
            ch2 <- i * i
        }
        close(ch2)
    }()
    // 在主goroutine中从ch2中接收值打印
    for i := range ch2 { // 通道关闭后会退出for range循环
        fmt.Println(i)
    }
}

从上面可以看出,有两种方式在接收值的时候判断通道是否被关闭;

一种是 判断 从通道取值时 ok 是否为 false,另一种是 for range的方式。

通常使用 for range 的方式。

8、单向通道

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

Go语言中提供了单向通道来处理这种情况。

1. `chan<- int`是一个只能发送的通道,可以发送但是不能接收;
2. `<-chan int`是一个只能接收的通道,可以接收但是不能发送。

使用示例:

// 只写
func count(ch1 chan<- int){
    for i := 0; i < 10; i++ {
        ch1 <- i
    }
    close(ch1)
}

// 既有只读,也有只写
func squarer(ch1 <-chan int, ch2 chan<- int){
    for{
        v,ok := <-ch1
        if ok {
            ch2<-v*v
        }else{
            break
        }
    }
    close(ch2)
}

func printer(ch2 <-chan int){
    for v := range ch2 {
        fmt.Println("v = ", v)
    }
}

func main(){
    // 单向通道,只读,或只写
    // 只读方式: <-chan 类型
    // 只写方式: chan<- 类型
    ch1 := make(chan int)
    ch2 := make(chan int)
    go count(ch1)
    go squarer(ch1, ch2)
    printer(ch2)
}

在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。

9、通道总结

channel常见的异常总结,如下图:

注意: 关闭已经关闭的channel也会引发panic。

作者:joker.liu  创建时间:2023-04-18 17:41
最后编辑:joker.liu  更新时间:2023-04-21 14:34