GO语言学习笔记-Channels
原文,建议先看完goroutine部分再看这篇。
什么是channels
channels可以理解成是goroutine之间通信的管道,和水流从管道的一端到另一端类似,数据也可以从管道的一端发送另一端接收。
声明channels
每个channel都需指定一个类型,这个类型是表明哪种类型的数据可以通过管道传输,而其他类型的不可以。
chan T
指接受类型T的channel。
channel的默认值是nil
,nil channel
不能被任何类型使用所以和map
或者slices
一样,要使用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)
}
}
上面声明了变量名为a
的channel,并且默认值是nil
,因此判断语句成立并且初始化类型为int
的channel,程序输出如下:
channel a is nil, going to define it
Type of a is chan int
通常我们使用一种更简洁的办法:
a := make(chan int)
发送和接收数据
语法如下:
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中的代码:
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重写一下:
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 goroutine
从done
中读取数据并且解除阻塞,接下来打印main function
。
程序输出如下:
Hello world goroutine
main function
再次引入Sleep
来更好的理解阻塞的概念:
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 done
和Main 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 goroutine
和cubes goroutine
中进行计算,并在main goroutine
中进行最后求和。(roy注:可以先自己实现再看下面的答案)
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注 直接读出来也行
}
calcSquares
和calcCubes
进行并发计算并把结果存储到相应的channel中,main goroutine
等待计算结果完成后输出:
Final output 1536
死锁
使用channel中一个非常重要的问题就是死锁,如果一个goroutine向channel中写入了数据,那么应该有其他的goroutine读取数据。如果没有,程序将报错Deadlock
。类似的,如果goroutine等待从channel中读取数据,那么应该有其他的goroutine向channel中写入数据,否则程序也将报错。
package main
func main() {
ch := make(chan int)
ch <- 5
}
上面的代码创建了channel ch
并向其中写入5,但并没有goroutine从ch
中读取数据,所以程序报错:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/sandbox249677995/main.go:6 +0x80
单向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)
}
上面的代码中我们创建了一个只能写入数据的单向channel。chan<- int
这个符号表明只能向这个channel写入数据,12行我们尝试从这个channel中读取数据,程序将会报错:
main.go:11: invalid operation: <-sendch (receive from send-only type 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)
}
上面程序中我们创建了一个双向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
。
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)
}
}
上面的程序producer
向chnl
中写入0-9后关闭channel,main
中使用for
循环来检查channel是否被关闭,如果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 range
来读取数据直到channel被关闭:
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
重写上面求和的程序来提高可重用性。如果你仔细观察,你将注意到从数字中提取某一位的代码是重复的,我们将这一步提取出来:
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
函数包含提取数字逻辑并且被calcSquares
和calcCubes
函数并发调用,一旦没有更多位数需要提取,channel将被关闭。calcSquares
和calcCubes
函数各自使用for range
循环读取channel中的数据,直到其被关闭。其他部分是一样的,程序将输出:
Final output 1536
接下来我们还要介绍关于channel更多的概念,比如buffered channels
、worker pools
、select
,欢迎持续关注。