上一节教程,我们讨论了Go语言是如何实现并发的。这节课我们将讨论通道以及协程间如何使用通道进行通信。

什么是通道?

通道可以想像成 Go 协程之间通信的管道。如同管道中的水会从一端流到另一端,通过使用信道,数据也可以从一端发送,在另一端接收。

通道的声明

每个通道都有一个类型,这个类型是通道允许传送的数据类型。其他数据类型不允许通过该通道传送。

chan T表示T类型的管道。

通道的零值是nil,零值是没有用的,因此应该想对map和切片那样,用make来定义通道。

让我们写一些代码来声明一个通道:

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)
    }
}
aa
channel a is nil, going to define it  
Type of a is chan int  

简短声明通常也是一种定义通道的简洁有效的方法。

a := make(chan int)  

这一行代码定义了一个int类型的通道a。

通过通道进行发送和接受

如下所示,该语法通过通道发送和接收数据。

data := <- a // 读取通道 a  
a <- data // 写入通道 a

通道旁的箭头方向指定了是发送数据还是接收数据。

aadata
aa

发送与接收默认是阻塞的

发送与接收默认是阻塞的。这是什么意思?当把数据发送到通道时,程序控制会在发送数据的语句处发生阻塞,直到有其它 Go 协程从信道读取到数据,才会解除阻塞。与此类似,当读取信通道的数据时,如果没有其它的协程把数据写入到这个信道,那么读取过程就会一直阻塞着。

通道的这种特性能够帮助 Go 协程之间进行高效的通信,不需要用到其他编程语言常见的显式锁或条件变量。

通道的代码示例

理论已经够了。接下来写点代码,看看协程之间通过通道是怎么通信的吧。

我们其实可以重写上章学习Go协程时写的程序,现在我们在这里用上通道。

首先引用前面教程里的程序。

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")
}

这是上一篇的代码。我们使用到了休眠,使 Go 主协程等待 hello 协程结束。如果你看不懂,建议你阅读上一教程Go协程。

我们接下来使用通道来重写上面代码。

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")
}
donedonehellodonedonetime.Sleep
<-done
donehellohelloHello world goroutinedonedonemain function

该程序输出如下:

Hello world goroutine  
main function
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")
}
hello
Main going to call hello go goroutinehellohello go routine is going to sleephello<-donedonehello go routine awake and going to write to doneMain received data

通道的另一个示例

我们再编写一个程序来更好地理解通道。该程序会计算一个数中每一位的平方和与立方和,然后把平方和与立方和相加并打印出来。

例如,如果输出是 123,该程序会如下计算输出:

squares = (1 * 1) + (2 * 2) + (3 * 3) 
cubes = (1 * 1 * 1) + (2 * 2 * 2) + (3 * 3 * 3) 
output = squares + cubes = 50

我们会这样去构建程序:在一个单独的 Go 协程计算平方和,而在另一个协程计算立方和,最后在 Go 主协程把平方和与立方和相加。

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)
}
calcSquaressquareopcalcCubescubop
squarescubes
Final output 1536

死锁

使用通道需要考虑的一个重点是死锁。当 Go 协程给一个通道发送数据时,照理说会有其他 Go 协程来接收数据。如果没有的话,程序就会在运行时触发 panic,形成死锁。

同理,当有 Go 协程等着从一个通道接收数据时,我们期望其他的 Go 协程会向该通道写入数据,要不然程序就会触发 panic。

package main

func main() {  
    ch := make(chan int)
    ch <- 5
}
chch <- 55ch
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:  
main.main()  
    /tmp/sandbox249677995/main.go:6 +0x80

单向通道

我们目前讨论的通道都是双向通道,即通过通道既能发送数据,又能接收数据。其实也可以创建单向通道,这种通道只能发送或者接收数据。

package main

import "fmt"

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

func main() {  
    sendch := make(chan<- int)
    go sendData(sendch)
    fmt.Println(<-sendch)
}
sendchchan<- intchan
main.go:11: invalid operation: <-sendch (receive from send-only type chan<- int)

一切都很顺利,只不过一个不能读取数据的唯送通道究竟有什么意义呢?

这就需要用到通道转换(Channel Conversion)了。把一个双向通道转换成唯送通道或者唯收(Receive Only)通道都是行得通的,但是反过来就不行。

package main

import "fmt"

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

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

关闭通道和使用 for range 遍历通道

数据发送方可以关闭通道,通知接收方这个通道不再有数据发送过来。

当从通道接收数据时,接收方可以多用一个变量来检查信道是否已经关闭。

v, ok := <- ch
okokint0
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)
    }
}
producerchn1okokokok
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 range 循环用于在一个通道关闭之前,从通道接收数据。

接下来我们使用 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)
    }
}
chch
Received  0  
Received  1  
Received  2  
Received  3  
Received  4  
Received  5  
Received  6  
Received  7  
Received  8  
Received  9

我们可以使用 for range 循环,重写通道的另一个示例这一节里面的代码,提高代码的可重用性。

calcSquarescalcCubes
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)
}
digitscalcSquarescalcCubesdigitscalcSquarescalcCubes
Final output 1536

本教程的内容到此结束。关于通道还有一些其他的概念,比如缓冲通道(Buffered Channel)、工作池(Worker Pool)和 select。我们会在接下来的教程里专门介绍它们。感谢阅读。祝您愉快。