通道
1.通道定义
通道(channels) 是连接多个协程的管道。 你可以从一个协程将值发送到通道,然后在另一个协程中接收。
定义:make(chan string)
发送消息:channel <-
接收消息:<-channel
package main
import "fmt"
func main() {
msgChan := make(chan string)
go func(){ msgChan <- "ping" }()
msg := <-msgChan
fmt.Println("msg",msg)
}
2.通道缓冲
默认情况下,通道是 无缓冲 的,这意味着只有对应的接收(<- chan) 通道准备好接收时,才允许进行发送(chan <-)。 有缓冲通道 允许在没有对应接收者的情况下,缓存一定数量的值。
package main
import "fmt"
func main() {
messages := make(chan string, 2)
messages <- "buffered"
messages <- "channel"
fmt.Println(<-messages)
fmt.Println(<-messages)
}
3.通道同步
我们可以使用通道来同步协程之间的执行状态。 这儿有一个例子,使用阻塞接收的方式,实现了等待另一个协程完成。
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, 1)
go worker(done)
<-done
}
如果你把 <- done 这行代码从程序中移除, 程序甚至可能在 worker 开始运行前就结束了。
4.通道方向
当使用通道作为函数的参数时,你可以指定这个通道是否为只读或只写。 该特性可以提升程序的类型安全。
定义只写通道:pings chan<- string
定义只读通道:pings <-chan string
package main
import "fmt"
func ping(pings chan<- string, msg string) {
pings <- msg
}
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)
}
-
ping
函数定义了一个只能发送数据的(只写)通道。 尝试从这个通道接收数据会是一个编译时错误。 -
pong
函数接收两个通道,pings 仅用于接收数据(只读),pongs 仅用于发送数据(只写)。
5.通道选择器
Go 的 选择器(select) 让你可以同时等待多个通道操作。 将协程、通道和选择器结合,是 Go 的一个强大特性。
当有多个通道的值需要接收时,如果用常规写法,则会根据代码书写前后阻塞的进行数据接收。也就是数据接收会有先后顺序。
如果使用select选择器,则可以同时接收多个通道的值。
代码示例:
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(3 * time.Second)
c1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()
// 常规写法
// msg1 := <-c1
// msg2 := <-c2
// fmt.Println("msg1:", msg1)
// fmt.Println("msg2:", msg2)
// select选择器
for i := 0; i < 2; i++ {
select {
case msg := <-c1:
fmt.Println("received", msg)
case msg := <-c2:
fmt.Println("received", msg)
}
}
}
// output
// 常规写法
//msg1: one
//msg2: two
// select选择器
//received two
//received one
常规写法特点
-
顺序执行:
- 代码会先从 c1 通道接收消息并赋值给 msg1,然后再从 c2 通道接收消息并赋值给 msg2。
- 如果 c1 或 c2 中没有消息,程序会阻塞在对应的接收操作上,直到通道有数据可用。
-
缺乏灵活性:
- 如果 c1 长时间没有消息,而c2已经有消息,程序仍然会卡在 c1 的接收操作上,无法处理 c2 的消息。
-
适合简单场景:
- 当你确定 c1 和c2都会按顺序提供消息时,这种写法是可以接受的。
select选择器特点
-
并发处理:
- select 语句会监听多个通道(c1和c2),并在任意一个通道有消息时执行对应的 case分支。如果c1和c2都没有消息,select 会阻塞,直到至少一个通道有数据。
-
灵活性更高:
- 无论 c1 还是 c2先收到消息,程序都会立即处理,而不会因为某个通道阻塞而卡住整个流程。
-
随机性:
- 如果多个通道同时有消息,select会随机选择一个通道执行对应的分支。
-
适合动态场景:
- 当你需要处理多个通道的消息,并且无法预知消息的到达顺序时,select 是更好的选择。
6.通道超时处理
超时 对于一个需要连接外部资源, 或者有耗时较长的操作的程序而言是很重要的。 得益于通道和 select,在 Go 中实现超时操作是简洁而优雅的。
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
c1 <- "result 1"
}()
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(1 * time.Second):
fmt.Println("timeout 1")
}
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
实现一个超时操作。res := <- c1
等待结果,<-time.After
等待超时(1秒钟)以后发送的值。 由于 select 默认处理第一个已准备好的接收操作, 因此如果操作耗时超过了允许的 1 秒的话,将会执行超时 case。 -
time.After
返回一个通道,在指定时间(这里是 1 秒)后会向该通道发送一个值。代码从这个通道中接收值并打印出来。需要注意的是,这里的 res 实际上是一个 time.Time 类型的值,表示超时发生的时间。
7.非阻塞通道操作
常规的通过通道发送和接收数据是阻塞的。 然而,我们可以使用带一个 default 子句的 select 来实现 非阻塞 的发送、接收,甚至是非阻塞的多路 select。
package main
import "fmt"
func main() {
messages := make(chan string)
signals := make(chan bool)
select {
case msg := <-messages:
fmt.Println("received message", msg)
default:
fmt.Println("no message received")
}
msg := "hi"
select {
case messages <- msg:
fmt.Println("sent message", msg)
default:
fmt.Println("no message sent")
}
select {
case msg := <-messages:
fmt.Println("received message", msg)
case sig := <-signals:
fmt.Println("received signal", sig)
default:
fmt.Println("no activity")
}
}
以上代码之所以会走default分支,是因为messages和signal通道都是无缓冲的通道,通道接受和发送都是阻塞的。
发送操作(messages <- msg)
会阻塞,直到有 Goroutine 从通道中接收数据。接收操作(<-messages)
会阻塞,直到有 Goroutine 向通道发送数据。
8.通道关闭
关闭 一个通道意味着不能再向这个通道发送值了。 该特性可以向通道的接收方传达工作已经完成的信息。
关闭通道:close(chan)
package main
import "fmt"
func main() {
jobs := make(chan int, 5)
done := make(chan bool)
go func() {
for {
j, more := <-jobs
if more {
fmt.Println("received job", j)
} else {
fmt.Println("received all jobs")
done <- true
return
}
}
}()
for j := 1; j <= 3; j++ {
jobs <- j
fmt.Println("sent job", j)
}
close(jobs)
fmt.Println("sent all jobs")
<-done
}
9.通道遍历
常见的通道遍历方法有以下两种:
-
使用
for
和range
循环for job := range jobs { fmt.Println("received job", job) } fmt.Println("received all jobs")
-
使用
for
和显式接收for { job, more := <-jobs if more { fmt.Println("received job", job) } else { fmt.Println("received all jobs") break } }