Go语言并发编程:深入理解select语句
本文深入解析 Go 语言中的 select 语句,涵盖空 select 导致的死锁、仅有 default 的非阻塞特性、case 匹配的随机性原理,以及如何通过 for + select 实现持续监听。
理解 Select 基础
select 是 Go 中专门用于处理 channel 的语句结构,类似于其他编程语言中的 switch 语句,但它只能用于 channel 操作。它的核心作用是在多个通道操作中选择一个可执行的进行。
第1步:了解空 Select 的阻塞行为
一个没有任何 case 和 default 的 select 语句会无限期阻塞当前 goroutine。由于没有其他任务在执行,这会导致程序整体触发死锁(fatal error)。这在开发初期调试时经常作为测试手段,但生产环境中必须避免。
package main
import (
"fmt"
"time"
)
/**
* 没有内容的select 会阻塞
* 没有内容是指,没有case,也没有default
* 如果没有其它的任务指执行,将会触发死锁
*/
func main() {
go func() {
time.Sleep(2 * time.Second)
fmt.Println("没有内容的select 会阻塞")
}()
select {}
}
第2步:掌握带有 Default 的非阻塞模式
如果 select 中只包含 default,它将不会阻塞。此时它的行为类似于串行化处理,立即执行默认分支。此外,当有多个 case 且都有数据可读时,Go 会随机选择一个 case 执行,而并非按照编写顺序。
package main
import "fmt"
/**
只有default的select 和串行化没有区别
*/
func main() {
go func() {
fmt.Println("quick")
}()
select {
default:
fmt.Println("end")
}
}
当既有 case 又有 default 时,若能匹配到 case 就执行对应的逻辑;若匹配不到(即没有数据准备),则执行 default。只要存在 default,select 就不会发生阻塞。
package main
import (
"fmt"
)
func main() {
ch1 := make(chan int, 2)
ch2 := make(chan int, 2)
select {
case v1 := <-ch1:
fmt.Println(v1)
case v2 := <-ch2:
fmt.Println(v2)
default:
fmt.Println(22)
}
}
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int, 2)
ch2 := make(chan int, 2)
go func() {
ch1 <- 1
}()
time.Sleep(1 * time.Second)
select {
case v1 := <-ch1:
fmt.Printf("get v1 chan value %d", v1)
case v2 := <-ch2:
fmt.Printf("get v1 chan value %d", v2)
default:
fmt.Println(22)
}
}
第3步:处理无 Default 时的长期阻塞
如果没有设置 default,select 将一直阻塞等待,直到某个 case 的数据就绪或被关闭。这种方式常用于需要“等到某件事发生”的场景。例如在下面的示例中,主 goroutine 会暂停约 3 秒,直到子进程向 ch1 发送数据后才会继续向下执行。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int, 2)
ch2 := make(chan int, 2)
fmt.Printf("start unix:%d \n", time.Now().Unix())
go func() {
time.Sleep(3 * time.Second)
ch1 <- 1
}()
select {
case v1 := <-ch1:
fmt.Printf("case unix: %d \n", time.Now().Unix())
fmt.Printf("get v1 chan value %d \n", v1)
case v2 := <-ch2:
fmt.Printf("get v1 chan value %d \n", v2)
}
fmt.Println("end")
}
第4步:使用 For-Select 循环实现持续监听
注意:单个 select 块仅会被求值一次。如果需要在多个通道上持续不断地轮询数据,必须结合 for 循环使用。这是 Go 中实现消息队列消费端或事件驱动服务的基础模式。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int, 2)
ch2 := make(chan int, 2)
fmt.Printf("start unix:%d \n", time.Now().Unix())
go func() {
for {
time.Sleep(1 * time.Second)
ch1 <- 1
}
}()
for {
select {
case v1 := <-ch1:
fmt.Printf("case unix: %d \n", time.Now().Unix())
fmt.Printf("get v1 chan value %d \n", v1)
case v2 := <-ch2:
fmt.Printf("get v1 chan value %d \n", v2)
}
}
}
第5步:验证 Case 匹配的随机无序性
当 select 中有多个 case 同时满足条件(即通道均有数据)时,Go 选择哪个分支是伪随机决定的。可以通过循环多次调用测试来观察这一现象:
package main
import (
"fmt"
"time"
)
func test() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
ch1 <- 1
close(ch1)
}()
go func() {
time.Sleep(1 * time.Second)
ch2 <- 1
close(ch2)
}()
time.Sleep(2 * time.Second)
// 如果有顺序,那么因该每次都是v1
select {
case v1 := <-ch1:
fmt.Printf("case unix: %d \n", time.Now().Unix())
fmt.Printf("get v1 chan value %d \n", v1)
case v2 := <-ch2:
fmt.Printf("case unix: %d \n", time.Now().Unix())
fmt.Printf("get v2 chan value %d \n", v2)
}
}
func main() {
for i := 0; i < 10; i++ {
test()
}
}
以上就是 Go 语言并发编程中关于 select 语句实现的详细内容。更多关于 Go 语言 golang 通道的运用技巧请关注本站其它相关文章!免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









