Go语言Channel

在本教程中,我们将讨论Channel以及 Goroutines 如何使用Channel进行通信。

什么是Channel

Channel可以被认为是 Goroutine 用来进行通信的管道。与水在管道中从一端流向另一端的方式类似,可以使用Channel从一端发送数据并从另一端接收数据。

声明Channel

每个Channel都有一个与其关联的类型。该类型是Channel允许传输的数据类型。不允许使用该Channel传输其他类型。

chan T是一个Channel类型是T

Channel的零值为nilnil的Channel没有任何用处,因此必须使用make类似于map和slices来定义Channel。

让我们编写一些声明Channel的代码。

package main

import "fmt"

func main() {  
    var a chan int
    if a == nil {
        fmt.Println("channel a is nil, going to define it")
        a = make(chan int)
        fmt.Printf("Type of a is %T", a)
    }
}

Run program in playground

第 1 行中声明的Channel,此时的Channel是nil。因此,会执行 if 条件内的语句并定义Channel。该程序将输出,

channel a is nil, going to define it  
Type of a is chan int  

与往常一样,简写声明也是定义Channel的有效且简洁的方式。

a := make(chan int)  

上面的代码行还定义了一个 int Channel变量a

从Channel发送和接收

下面给出了从Channel发送和接收数据的语法:

data := <- a // read from channel a  
a <- data // write to channel a  

箭头相对于Channel的方向指定是发送还是接收数据。

在第一行中,箭头指向外部a,因此我们从Channel读取a并将值存储到变量data

在第二行中,箭头指向a,因此我们正在向写入通道写入a

默认情况下发送和接收是阻塞的

默认情况下,向Channel发送和接收是阻塞的。这是什么意思?当数据发送到Channel时,控制会在发送语句中被阻塞,直到其他 Goroutine 从该Channel读取数据。类似地,当从Channel读取数据时,读取会被阻塞,直到某个 Goroutine 将数据写入该Channel。

Channel的这一属性有助于 Goroutines 有效地进行通信,而无需使用其他编程语言中很常见的显式锁或条件变量。

如果现在这没有意义也没关系。接下来的部分将更清楚地说明默认情况下Channel是如何阻塞的。

Channel示例程序

让我们编写一个程序来了解 Goroutine 如何使用Channel进行通信。

让我引用上一篇教程中的程序。

package main

import (  
    "fmt"
    "time"
)

func hello() {  
    fmt.Println("Hello world goroutine")
}
func main() {  
    go hello()
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}

Run program in playground

这是上一个教程中的程序。我们在这里使用 sleep 来让主 Goroutine 等待 hello Goroutine 完成。

我们将使用Channel重写上面的程序。

package main

import (  
    "fmt"
)

func hello(done chan bool) {  
    fmt.Println("Hello world goroutine")
    done <- true
}
func main() {  
    done := make(chan bool)
    go hello(done)
    <-done
    fmt.Println("main function")
}

Run program in playground

在上面的程序中,我们在第 1 行创建了一个布尔Channel。 并将其作为参数传递给helloGoroutine。在14行号中。 我们正在从doneChannel接收数据。这行代码是阻塞的,这意味着在某个 Goroutine 将数据写入Channel之前done,控件不会移动到下一行代码。因此,这消除了原始程序中存在的用于防止主 Goroutine 退出的需要time.Sleep

该代码行<-done从完成的Channel接收数据,但不使用该数据或将该数据存储在任何变量中。这是完全合法的。

现在我们的mainGoroutine 被阻塞,等待完成Channel上的数据。Goroutinehello接收此Channel作为参数,打印Hello world goroutine然后写入done。当此写入完成时,主 Goroutine 会从 did Channel接收数据,解除阻塞,然后打印主函数。

该程序输出

Hello world goroutine  
main function  

让我们通过在 Goroutine 中引入 sleep 来修改此程序,hello以更好地理解这种阻塞概念。

package main

import (  
    "fmt"
    "time"
)

func hello(done chan bool) {  
    fmt.Println("hello go routine is going to sleep")
    time.Sleep(4 * time.Second)
    fmt.Println("hello go routine awake and going to write to done")
    done <- true
}
func main() {  
    done := make(chan bool)
    fmt.Println("Main going to call hello go goroutine")
    go hello(done)
    <-done
    fmt.Println("Main received data")
}

Run program in playground

在上面的程序中,我们在第 10 行的函数中引入了 4 秒的睡眠。.

该程序将首先打印Main going to call hello go goroutine. 然后 hello Goroutine 将启动并打印hello go routine is going to sleep。打印完此信息后,helloGoroutine 将休眠 4 秒,在此期间,mainGoroutine 将被阻塞,因为它正在等待第 18 行完成Channel的数据<-donehello go routine awake and going to write to done后将打印,然后是Main received data.

Main going to call hello go goroutine
hello go routine is going to sleep
hello go routine awake and going to write to done
Main received data

Channel的另一个例子

让我们再编写一个程序来更好地理解Channel。该程序将打印一个数字的各个数字的平方和和立方之和。

例如,如果输入为 123,则该程序将计算输出为

正方形 = (1 * 1) + (2 * 2) + (3 * 3)
立方体 = (1 * 1 * 1) + (2 * 2 * 2) + (3 * 3 * 3)
输出 = 正方形 + 立方体 = 50

我们将构建该程序,使得平方在单独的 Goroutine 中计算,立方体在另一个 Goroutine 中计算,最终求和发生在主 Goroutine 中。

package main

import (  
    "fmt"
)

func calcSquares(number int, squareop chan int) {  
    sum := 0
    for number != 0 {
        digit := number % 10
        sum += digit * digit
        number /= 10
    }
    squareop <- sum
}

func calcCubes(number int, cubeop chan int) {  
    sum := 0 
    for number != 0 {
        digit := number % 10
        sum += digit * digit * digit
        number /= 10
    }
    cubeop <- sum
} 

func main() {  
    number := 589
    sqrch := make(chan int)
    cubech := make(chan int)
    go calcSquares(number, sqrch)
    go calcCubes(number, cubech)
    squares, cubes := <-sqrch, <-cubech
    fmt.Println("Final output", squares + cubes)
}

Run program in playground

第 7行中的函数calcSquares。 计算该数字各个数字的平方和并将其发送到squareop。同样,第 17 行中的calcCubes函数。计算数字各个数字的立方和并将其发送到cubeop

这两个函数在第31行 和 32行中作为单独的 Goroutine 运行。每个Channel都传递一个要写入的参数作为参数。主 Goroutine 在第 33 行等待来自这两个Channel的数据。 一旦从两个Channel接收到数据,它们就会被存储在squarescubes变量中,并计算和打印最终输出。该程序将打印

Final output 1536  

死锁

使用Channel时要考虑的一个重要因素是死锁。如果一个 Goroutine 正在Channel上发送数据,那么预计其他一些 Goroutine 应该正在接收数据。如果这没有发生,那么程序将在运行时出现Deadlock

类似地,如果一个 Goroutine 正在等待从某个Channel接收数据,那么其他一些 Goroutine 就应该在该Channel上写入数据,否则程序将会出现Deadlock

package main


func main() {  
    ch := make(chan int)
    ch <- 5
}

Run program in playground

在上面的程序中,ch创建了一个Channel,我们在第6 行发送5到该Channel。在此程序中,没有其他 Goroutine 正在从Channel接收数据。因此,该程序将因以下运行时错误而发生恐慌。

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:  
main.main()  
    /tmp/sandbox046150166/prog.go:6 +0x50

单向Channel

到目前为止我们讨论的所有Channel都是双向Channel,即数据可以在它们上发送和接收。还可以创建单向Channel,即仅发送或接收数据的Channel。

package main

import "fmt"

func sendData(sendch chan<- int) {  
    sendch <- 10
}

func main() {  
    sendch := make(chan<- int)
    go sendData(sendch)
    fmt.Println(<-sendch)
}

Run program in playground

在上面的程序中,我们sendch在第 10 行创建仅发送Channel。chan<- int箭头指向 时表示仅发送Channel。我们尝试从在的仅发送Channel接收数据。这是不允许的,当程序运行时,编译器会,

./prog.go:12:14:无效操作:<-sendch(从仅发送类型 chan<- int 接收)

一切都很好,但如果无法读取,写入仅发送Channel有什么意义!

这就是Channel转换发挥作用的地方。可以将双向Channel转换为仅发送或仅接收Channel,但反之则不然。

package main

import "fmt"

func sendData(sendch chan<- int) {  
    sendch <- 10
}

func main() {  
    chnl := make(chan int)
    go sendData(chnl)
    fmt.Println(<-chnl)
}

Run program in playground

上面程序的在10行号中。chnl创建了一个双向Channel。它作为参数传递给sendData Goroutine。. 该sendData函数在第 6行将此Channel转换为仅发送Channel。所以现在Channel仅在sendDataGoroutine 内部发送,但在主 Goroutine 中是双向的。该程序将打印10为输出。

关闭Channel和Channel上的范围循环

发送方能够关闭Channel,以通知接收方不再有数据在Channel上发送。

接收者在从Channel接收数据时可以使用附加变量来检查Channel是否已关闭。

v, ok := <- ch  

ok如果通过成功发送到Channel的操作接收到该值,则上述语句为 true。如果ok为 false,则意味着我们正在从封闭的Channel中读取数据。从关闭的Channel读取的值将是该Channel类型的零值。例如,如果Channel是一个intChannel,那么从关闭的Channel接收到的值将为0

package main

import (  
    "fmt"
)

func producer(chnl chan int) {  
    for i := 0; i < 10; i++ {
        chnl <- i
    }
    close(chnl)
}
func main() {  
    ch := make(chan int)
    go producer(ch)
    for {
        v, ok := <-ch
        if ok == false {
            break
        }
        fmt.Println("Received ", v, ok)
    }
}

Run program in playground

在上面的程序中,producerGoroutine 将 0 到 9 写入chnlChannel,然后关闭Channel。main 函数for在第 16 行有一个无限循环,它使用第 16 行中的变量检查Channel是否关闭ok。如果ok为 false,则表示Channel已关闭,因此循环已中断。ok否则打印接收到的值和 的值。该程序打印,

Received  0 true  
Received  1 true  
Received  2 true  
Received  3 true  
Received  4 true  
Received  5 true  
Received  6 true  
Received  7 true  
Received  8 true  
Received  9 true  

for循环的for range形式可用于从Channel接收值,直到Channel关闭为止。

让我们使用 for range 循环重写上面的程序。

package main

import (  
    "fmt"
)

func producer(chnl chan int) {  
    for i := 0; i < 10; i++ {
        chnl <- i
    }
    close(chnl)
}
func main() {  
    ch := make(chan int)
    go producer(ch)
    for v := range ch {
        fmt.Println("Received ",v)
    }
}

Run program in playground

第 16 行中的循环for range。 从Channel接收数据ch直至关闭。一旦ch关闭,循环就会自动退出。该程序输出,

Received  0  
Received  1  
Received  2  
Received  3  
Received  4  
Received  5  
Received  6  
Received  7  
Received  8  
Received  9  

可以使用 for range 循环重写Channel部分的另一个示例中的程序,以提高代码可重用性。

如果仔细观察该程序,您会发现查找数字的各个数字的代码在calcSquares函数和calcCubes函数中都重复出现。我们将该代码移至其自己的函数中并同时调用它。

package main

import (  
    "fmt"
)

func digits(number int, dchnl chan int) {  
    for number != 0 {
        digit := number % 10
        dchnl <- digit
        number /= 10
    }
    close(dchnl)
}
func calcSquares(number int, squareop chan int) {  
    sum := 0
    dch := make(chan int)
    go digits(number, dch)
    for digit := range dch {
        sum += digit * digit
    }
    squareop <- sum
}

func calcCubes(number int, cubeop chan int) {  
    sum := 0
    dch := make(chan int)
    go digits(number, dch)
    for digit := range dch {
        sum += digit * digit * digit
    }
    cubeop <- sum
}

func main() {  
    number := 589
    sqrch := make(chan int)
    cubech := make(chan int)
    go calcSquares(number, sqrch)
    go calcCubes(number, cubech)
    squares, cubes := <-sqrch, <-cubech
    fmt.Println("Final output", squares+cubes)
}

Run program in playground

上面程序中的函数digits现在包含从数字中获取各个数字的逻辑,并且由 和calcSquares函数calcCubes同时调用。一旦号码中不再有数字,则第13 行Channel将关闭。.calcSquarescalcCubesGoroutine 使用for range循环监听各自的Channel,直到关闭。程序的其余部分是相同的。该程序还将打印

Final output 1536  

本教程到此结束。Channel中还有一些概念,例如缓冲Channel、工作池和select。我们将在他们自己的单独教程中讨论它们。谢谢阅读。祝你有美好的一天。

  • 16
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值