原文,建议先看完goroutine部分再看这篇。

什么是channels

channels可以理解成是goroutine之间通信的管道,和水流从管道的一端到另一端类似,数据也可以从管道的一端发送另一端接收。

声明channels

每个channel都需指定一个类型,这个类型是表明哪种类型的数据可以通过管道传输,而其他类型的不可以。

chan T指接受类型T的channel。

channel的默认值是nilnil channel不能被任何类型使用所以和map或者slices一样,要使用make关键字来进行定义。

1
2
3
4
5
6
7
8
9
10
11
12
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)
}
}

上面声明了变量名为a的channel,并且默认值是nil,因此判断语句成立并且初始化类型为int的channel,程序输出如下:

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

通常我们使用一种更简洁的办法:

1
a := make(chan int)

发送和接收数据

语法如下:

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

箭头指向的方向表明了是读取还是接收数据。第一行,箭头向外指出,所以代表从a中读取数据并赋值给data变量。第二行,箭头指向a因此是向a中写入数据。

读写默认是阻塞行为

对channel进行读写操作默认是阻塞的,什么意思呢?当向channel写入数据时,程序被阻塞在写数据的语句处,直到有其他的goroutine从channel中读取。同样的,当从channel中读数据也会阻塞直到其他goroutine向其中写数据。

这种特性帮助goroutine之间进行高效通信,而不用像其他编程语言中那样使用显示声明锁或者条件变量来实现。

例子

说完理论,我们来编写代码看看goroutine如何使用channel进行通信。我们先复习一下上一篇学习goroutine中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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")
}

我们使用Sleep来阻塞了main goroutine等待hello goroutine执行完毕,如果你对这个不理解,请看前一篇文章

我们使用channel重写一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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")
}

上面程序中我们创建了名为done的布尔型channel并作为参数传递给了hello这个goroutine,14行我们从done中读取数据,这行代码将被阻塞直到其他的goroutine向其中写入数据,因此不再需要Sleep来阻止main goroutine继续执行了。

<-done这行代码从channel中读取数据但不使用任何变量存储,这是符合语法的。

现在main goroutine被阻塞,等待done中的数据,hello接收这个channel作为参数, 输出Hello world goroutine并且向done中写数据。当写入完成后,main goroutinedone中读取数据并且解除阻塞,接下来打印main function

程序输出如下:

1
2
Hello world goroutine  
main function

再次引入Sleep来更好的理解阻塞的概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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中sleep4秒钟。

程序首先会输出Main going to call hello go goroutine并且启动goroutine并输出hello go routine is going to sleep。之后hello goroutine被阻塞4秒钟,与此同时main goroutine也会被阻塞,因为它需要在done中读取数据。4秒钟后将输出ello go routine awake and going to write to doneMain received data

其他例子

让我们来写一个更复杂的例子来理解channel,这个程序要求,输入一个数字,输出其每一位的平方和与立方和,并对这2者求和。比如我们输入123,则

squares = (1 1) + (2 2) + (3 * 3)

cubes = (1 1 1) + (2 2 2) + (3 3 3)

output = squares + cubes = 50

我们分别在squares goroutinecubes goroutine中进行计算,并在main goroutine中进行最后求和。(roy注:可以先自己实现再看下面的答案)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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)
// fmt.Println("Final output ", <-sqrch+<-cubech) roy注 直接读出来也行
}

calcSquarescalcCubes进行并发计算并把结果存储到相应的channel中,main goroutine等待计算结果完成后输出:

Final output 1536

死锁

使用channel中一个非常重要的问题就是死锁,如果一个goroutine向channel中写入了数据,那么应该有其他的goroutine读取数据。如果没有,程序将报错Deadlock。类似的,如果goroutine等待从channel中读取数据,那么应该有其他的goroutine向channel中写入数据,否则程序也将报错。

1
2
3
4
5
6
7
package main


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

上面的代码创建了channel ch并向其中写入5,但并没有goroutine从ch中读取数据,所以程序报错:

1
2
3
4
5
fatal error: all goroutines are asleep - deadlock!

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

单向channel

至今我们讨论的channel都是双向的,既可以写入也可读取数据。创建单向channel也是可以的,单向channel只能写入或者读取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

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

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

上面的代码中我们创建了一个只能写入数据的单向channel。chan<- int这个符号表明只能向这个channel写入数据,12行我们尝试从这个channel中读取数据,程序将会报错:

1
main.go:11: invalid operation: <-sendch (receive from send-only type chan<- int)

但是呢,这种只能写入不能读取的channel有毛用啊?

有一种使用情况就是在channel转换时。我们可以将双向channel转换成单向channel,但反过来则不可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

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

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

上面程序中我们创建了一个双向channel chnl,并把他作为参数传递给sendData,第5行函数通过sendch chan<- int将其转换为单向channel,所以在函数内部这个channel只能写入数据,而在main函数里chnl依然是个双向channel。程序将输出10

关闭channel和range循环

发送方可以关闭channel来通知接收方没有更多数据传递了,而接收方可以使用额外的变量获取channel是否被关闭。

v, ok := <- ch

上面的代码中,如果ok的值是true则代表成功从channel获取到了值,为false则代表从一个已经关闭了的channel中读取数据,读取到的值为channel类型的默认值。比如从关闭的int类型的channel读取到的值是0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)
}
}

上面的程序producerchnl中写入0-9后关闭channel,main中使用for循环来检查channel是否被关闭,如果ok值为false则代表channel被关闭并且跳出循环,否则输出读取的值和ok

1
2
3
4
5
6
7
8
9
10
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来读取数据直到channel被关闭:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)
}
}

一旦channel关闭,循环将自动结束。程序输出和上面一样。

我们可以使用for range重写上面求和的程序来提高可重用性。如果你仔细观察,你将注意到从数字中提取某一位的代码是重复的,我们将这一步提取出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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)
}

digits函数包含提取数字逻辑并且被calcSquarescalcCubes函数并发调用,一旦没有更多位数需要提取,channel将被关闭。calcSquarescalcCubes函数各自使用for range循环读取channel中的数据,直到其被关闭。其他部分是一样的,程序将输出:

1
Final output 1536

接下来我们还要介绍关于channel更多的概念,比如buffered channelsworker poolsselect,欢迎持续关注。