欢迎来到飞鸟慕鱼博客,开始您的技术之旅!
当前位置: 首页知识笔记正文

go语言channel 的用法,go语言channel聊天室

终极管理员 知识笔记 80阅读

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

什么是Channel

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

声明Channel

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

chan T是一个Channel类型是T

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

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

package mainimport fmtfunc 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 mainimport (      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 mainimport (      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 mainimport (      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 goroutinehello go routine is going to sleephello go routine awake and going to write to doneMain 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 mainimport (      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 mainfunc 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 mainimport fmtfunc 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 mainimport fmtfunc 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 mainimport (      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 mainimport (      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 mainimport (      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, squarescubes)}

Run program in playground

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

Final output 1536  

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

标签:
声明:无特别说明,转载请标明本文来源!