channels
channel 是连接多个 go 程的管道,你可以使用一个 go 程去给 channel 发送值并且使用另一个 go 程接收值。
声明channel
我们使用 make(chan val-type)
的语法来声明一个 channel。
package main
import "fmt"
func main() {
// 声明一个 channel 采用 make(chan val-type) 语法
messages := make(chan string)
// 向 channel中添加值采用 channel <- value 语法
go func() { messages <- "hello channel" }()
// 消费 channel 中的信息采用 <- channel 语法
msg := <-messages
fmt.Println(msg)
}
我们声明的 channel 是非缓冲的,需要有收方和发方,否则会阻塞,如果我们将上面的 go 程取消掉,而都在 main 程中进行,就会报错,因为在程序第十行,我们向 channel 中发送了一个值,但是没有接受者,就会阻塞。
package main
import "fmt"
func main() {
// 声明一个 channel 采用 make(chan val-type) 语法
messages := make(chan string)
// 向 channel中添加值采用 channel <- value 语法
// go func() { messages <- "hello channel" }()
messages <- "hello channel"
// 消费 channel 中的信息采用 <- channel 语法
msg := <-messages
fmt.Println(msg)
}
报错信息如下:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/root/goProject/src/github.com/nansl/goByExample/channels/channels.go:10 +0x59
exit status 2
缓冲 channel
默认的 channel 是需要同时有接收方和发送方的。Go 提供了一种带缓冲的 channel,它会有一个容量,未达到容量时,不会阻塞。
如下是 Go语言中文网 上的一篇文章,可以帮助理解 Buffered channels,原文链接点击这里。
package main
import (
"fmt"
"time"
)
func write(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
fmt.Println("successfully wrote", i, "to ch")
}
close(ch)
}
func main() {
ch := make(chan int, 2)
go write(ch)
time.Sleep(2 * time.Second)
for v := range ch {
fmt.Println("read value", v,"from ch")
time.Sleep(2 * time.Second)
}
}
上述例子输出如下:
successfully wrote 0 to ch
successfully wrote 1 to ch
read value 0 from ch
successfully wrote 2 to ch
read value 1 from ch
successfully wrote 3 to ch
read value 2 from ch
successfully wrote 4 to ch
read value 3 from ch
read value 4 from ch
当我们成功向 channel 中发送值时,会打印 successfully 信息,我们发现打印两个之后,主线程开始消费信息,在此之前 time.Sleep(2 * time.Second),也就是,在向 ch 发送两条信息后,发生了阻塞,因为达到了 ch 设定的容量。
所以:非阻塞 channel 在内部元素不到容量时,不会发生阻塞,在达到容量范围时,会阻塞,等待其他 go 程消费信息。
管道同步
我们可以利用管道来实现同步,利用的是读不到管道内容时阻塞的机制。
package main
import (
"fmt"
"time"
)
func worker(done chan bool) {
fmt.Print("working...")
time.Sleep(time.Second)
fmt.Println("done")
done <- true
}
func main() {
done := make(chan bool)
go worker(done)
<-done
}
该例子中, main 程要接收 done
中的值,可是 worker()
需要等待自身执行完毕后才会向 done
发送值,达到了同步的作用,即 worker()
执行完后会通知其他 go 程。
通道方向
上面我们使用函数来接收管道,但有时我们希望该函数内部只允许对管道进行收或者发,我们可以规定管道的收发:
package main
import "fmt"
// 该方法规定了 该方法只允许向管道中发送数据
func ping(pings chan<- string, msg string) {
pings <- msg
}
// 该方法规定只允许接收 pings,并且只允许向 pongs 发送
func pong(pings <-chan string, pongs chan<- string) {
msg := <-pings
pongs <- msg
}
func main() {
pings := make(chan string, 1)
pongs := make(chan string, 1)
ping(pings, "passed message")
pong(pings, pongs)
fmt.Println(<-pongs)
}
程序输出如下:
passed message
Select
Go 的 select 让你等待多管道
package main
import (
"fmt"
"time"
)
func main() {
// 声明两个管道
c1 := make(chan string)
c2 := make(chan string)
// 分别等待一段时间来模拟一次RPC调用
go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()
// 如果去掉for循环,select 捕获到c1之后程序终止,
// 所以循环两次来将两个管道全部捕获
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
}
Timeouts
超时对于那些要连接外部资源或者要绑定执行时间的程序来说是重要的。在Go中我们可以使用select
和 channel
来很方便的实现timeout
package main
import (
"fmt"
"time"
)
func main() {
// 建立一个缓冲 channel
c1 := make(chan string, 1)
// 因为是一个 buffered channel 所以在发送给 c1 值后,
// 该go程不会阻塞,而是会结束,避免go程从未被读取而泄漏
go func() {
time.Sleep(2 * time.Second)
c1 <- "result 1"
}()
// 这里声明了一个select来接收 res,但是如果超过了 1S,
// select将接收到 time,此时可以处理超时的逻辑
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(1 * time.Second):
fmt.Println("timeout 1")
}
// 超时时间设置为3s,该go程执行需要2s,所以不会触发超时。
c2 := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
c2 <- "result 2"
}()
select {
case res := <-c2:
fmt.Println(res)
case <-time.After(3 * time.Second):
fmt.Println("timeout 2")
}
}
该例子展示了如何利用 select
以及 channel
实现一个超时控制逻辑。
非阻塞的select
上面例子中的 select 在选择 channel 时,如果当前没有管道可以进行接收或者发送操作,那么就会阻塞,这个例子将讲解如何实现非阻塞的select。
正如switch,如果在没有满足条件的 case 时,会执行default,select也具备一样的特性。
如下就是一个非阻塞的select
package main
import "fmt"
func main() {
messages := make(chan string)
signals := make(chan bool)
// 此时,message中并没有数据,所以不会接收到信息
// 执行default内的内容
select {
case msg := <-messages:
fmt.Println("received message", msg)
default:
fmt.Println("no message received")
}
// 这里也不会执行第一个case,因为message是阻塞 channel
// 在没有接受者时,会发生阻塞而不会向channel发送数据
msg := "hi"
select {
case messages <- msg:
fmt.Println("sent message", msg)
default:
fmt.Println("no message sent")
}
// 也可以接受多个case,这里我们尝试对msssages以及signals进行非阻塞接收
select {
case msg := <-messages:
fmt.Println("received message", msg)
case sig := <-signals:
fmt.Println("received signal", sig)
default:
fmt.Println("no activity")
}
}
该程序的输出如下:
no message received
no message sent
no activity
关闭channels
关闭管道表示将不会在这个管道发送数据,但是仍然可以进行接收,接收者可以捕获通道中不含元素且通道已经关闭。
在这个例子中,我们将使用 jobs
管道来从 main 线程传达将要做的工作给一个 worker go 程,该管道将在没有工作可以传达时关闭:
package main
import "fmt"
func main() {
jobs := make(chan int, 5)
done := make(chan bool)
// 循环从 jobs管道中接收数据,more是一个bool 类型,
// 将在管道关闭且管道中没有数据时返回false
go func() {
for {
j, more := <-jobs
if more {
fmt.Println("received job", j)
} else {
fmt.Println("received all jobs")
done <- true
return
}
}
}()
// 使用一个阻塞的管道来接收go程执行完成的通知
for j := 1; j <= 3; j++ {
jobs <- j
fmt.Println("sent job", j)
}
close(jobs)
fmt.Println("sent all jobs")
<-done
}
遍历管道
我们可以使用 range
遍历map 或者 slice,我们也可以使用其遍历管道,它将在管道关闭且管道内没有元素的时候停止。
package main
import "fmt"
func main() {
queue := make(chan string, 2)
queue <- "one"
queue <- "two"
close(queue)
for elem := range queue {
fmt.Println(elem)
}
for elem := range queue {
fmt.Println(elem)
}
fmt.Println(<-queue)
fmt.Println(len(queue))
}
上述例子在向 queue
中放入两个值后,利用 range
来迭代 queue
,它将在迭代完元素之后停止。
range
是将管道中的消息消费了,下面的 for range
并不会触发,因为 queue
的长度是0且管道已经关闭了。
输出如下:
one
two
0