循环与Select
1. select
select 语句会从上到下依次检查每个 case 的通信操作语句,每个case必须是一个通信操作,要么接收,要么发送。
- 如果发现某个
case的通信操作可以立即执行,就会执行该case语句块并跳出select代码块。 - 如果多个
case均可执行,则会随机选择一个执行。 - 如果没有任何一个
case可以执行,则会执行default语句块(如果存在的话)。<span style="background:#fff88f">如果select语句带default并且放在无限循环 中,就会变成忙轮询(busy waiting),导致高CPU占用。 - 如果没有default,select会被阻塞,goroutine会进入休眠状态,直到某个case满足条件。
func main() {
// 无缓冲channel
chan1 := make(chan int)
chan2 := make(chan string)
// 有缓冲channel
chan3 := make(chan string, 5) // 缓冲区大小为5
var data = "test"
go func() {
for {
select {
// <-chan1 是一个接收操作符号。如果chan1通道中有数据可以读取,那么就会执行这个case并丢弃读到的数据(仅当做一种触发信号),如果当前没有数据可读,则不会阻塞,而是继续检查下一个case。
case <-chan1:
fmt.Println("receive from chan1.")
// 从chan3中读取数据赋值给tmp变量
case tmp := <-chan3:
fmt.Printf("receive from chan3, value %s\n", tmp)
// chan2 <- data 是一个发送操作符号,其中data是要发送的数据。如果chan2通道可以立即发送数据,那么就会执行这个case下面的代码块。如果当前通道已满无法发送,则不会阻塞,而是继续检查下一个case。
// 这里chan2未定义缓冲大小,如果没有其他goroutine从chan2中接收数据的会,这个发送动作会一直阻塞
// 如果要在无接收者的情况下实现往chan2中发送数据,则最好给chan2定义缓冲大小
case chan2 <- data:
fmt.Println("send data to chan2")
}
}
}()
// 持续向chan1发送数据,以触发select中的case
go func() {
for i := 0; i < 3; i++ {
chan1 <- i
}
}()
// 持续从chan2中接收数据,以触发select中的case
go func() {
for i := 0; i < 3; i++ {
var _ = <-chan2
}
}()
// 持续向chan3发送数据,以触发select中的case
go func() {
for i := 0; i < 5; i++ {
chan3 <- fmt.Sprintf("data%d", i)
}
}()
time.Sleep(time.Second * 10)
// 这里需要关闭chan,以防止goroutine泄漏
close(chan1)
close(chan2)
close(chan3)
}
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 3; i++ {
ch <- i
}
close(ch)
}()
// 从一个已经关闭的chan中获取数据不会阻塞,会得到0,如下代码会打印 Received:1 Received: 2 Received: 3,之后一直死循环打印Received: 0
//for { // val := <-ch // time.Sleep(time.Second) // fmt.Println("Received:", val) //} // 从channel中获取数据时最好判断channel是否关闭了,除非你确定chan不会关闭
for {
// ok为bool类型,代表chan是否关闭
val, ok := <-ch
if !ok {
fmt.Println("Channel closed")
break
}
fmt.Println("Received:", val)
}
}
超时判断
func main() {
//比如在下面的场景中,使用全局resChan来接受response,如果时间超过3S,resChan中还没有数据返回,则第二条case将执行
var resChan = make(chan int)
// do request
select {
case data := <-resChan:
fmt.Println(data)
case <-time.After(time.Second * 3):
fmt.Println("request time out")
}
}
2. for select case陷阱
在Go语言中,select语句用于在多个channel操作中进行选择,它本身不会导致CPU busy,但某些使用方式可能会造成高CPU占用。
2.1. 正确用法
如果select语句阻塞等待通道操作,Go运行时会让出CPU,避免忙等待
func worker(ch chan int) {
for {
select {
case val := <-ch:
fmt.Println("Received:", val)
}
}
}
这里select会阻塞,直到ch有数据可读。阻塞时,Go运行时会让出CPU,不会造成busy waiting。
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
21381 devbox 20 0 1225400 2024 1276 S 0.0 0.0 0:00.00 main
2.2. 错误用法
如果 select 语句在循环中不断轮询且没有阻塞,可能会导致高 CPU 占用。例如:
func busyWorker(ch chan int) {
for {
select {
case val := <-ch:
fmt.Println("Received:", val)
default:
// 没有数据时,不做任何阻塞操作,导致无限循环
}
}
}
default分支会在没有数据可读时立即执行,导致for循环不停执行,CPU占用率飙高。
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
21941 devbox 20 0 1225400 2028 1276 R 99.0 0.0 0:13.83 main
简单优化
for {
select {
case val := <-ch:
fmt.Println("Received:", val)
default:
time.Sleep(10 * time.Millisecond) // 让出 CPU,避免忙等待
}
}
增加sleep,让出cpu调度
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
22872 devbox 20 0 1225656 2020 1276 S 0.7 0.0 0:00.03 main
在Go语言的select语句中,如果 select 语句内包含default分支,并且所有其他 case 都没有准备好(没有数据可读或可写),那么default立即执行,不会阻塞。
3. for循环调度新协程
当在for循环中启动协程并使用循环变量时,所有协程可能最终会使用循环结束时变量的最终值,而不是启动协程时的值。这是因为:
- 变量作用域:循环变量在整个循环中是同一个变量(同一个内存地址)
- 闭包捕获:Go 协程创建了一个闭包,捕获的是变量的引用而非值
- 执行时机:协程可能在循环结束后才真正开始执行
Go 1.22 之前:循环变量在每次迭代中是重用的(相同的内存地址),会导致协程捕获到相同引用。
Go 1.22 及以后:默认情况下,每次迭代会为循环变量创建一个新的变量实例。这意味着在协程中捕获这些变量时,每个协程会捕获到不同的变量实例。
go 1.22之前错误示例:
func main() {
values := []int{1, 2, 3, 4, 5}
for i, val := range values {
go func() {
// 错误:直接使用循环变量
fmt.Printf("索引: %d, 值: %d\n", i, val)
}()
}
time.Sleep(time.Second) // 等待协程执行
}
go 1.22版本以前输出示例:
索引: 4, 值: 5
索引: 4, 值: 5
索引: 4, 值: 5
索引: 4, 值: 5
索引: 4, 值: 5