Golang中select语句的使用问题
Golang中select语句的使用问题 你好,
我在下面代码中使用 select 语句时遇到了一个问题:
package main
import (
"fmt"
"time"
)
func GenInts() <-chan int {
c := make(chan int)
go func() {
defer close(c)
for i:=0; i < 10; i++ {
c <- i
}
}()
return c
}
func guard(cond bool, c chan<- int) chan<- int {
if !cond {
return nil
} else {
return c
}
}
func EvenCheck(ints <-chan int) <-chan int {
c := make(chan int)
go func() {
defer close(c)
for n := range ints {
select {
case guard(n % 2 == 0, c) <- n:
default:
continue
}
time.Sleep(time.Millisecond)
}
}()
return c
}
func main() {
for n := range EvenCheck(GenInts()) {
fmt.Println(n)
}
}
我想利用通道的特性:当其值为 nil 时,会阻塞任何写入(或读取,虽然这里没有用到)操作。
通过 guard 函数,我检查数字是否为偶数,如果是,则将该数字发送到输出通道。
如果不是,则继续处理下一个整数。
我观察到了 select 语句一个奇怪的行为(至少我认为是)。
如果我在 select 循环中不引入延迟,这段代码就无法正常工作。
然而,只要加入一个短暂的延迟,代码就能正常工作了。
此代码在 Go 1.13.4 下测试。
有人能解释一下原因吗? …
更多关于Golang中select语句的使用问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html
calmh:
你所说的“如果已经有接收者准备好接收,就向这个通道发送 n,否则就继续执行”。如果
guard返回nil,那么就没有接收者准备好,我们确实会继续执行。如果guard返回一个通道,那么该通道可能有接收者准备好,但在你的情况下,接收者的 goroutine 很可能正忙于执行fmt.Println而无法接收。在这种情况下,我们同样会跳过发送。你添加的延迟使得接收者的 goroutine 更有可能有时间完成打印并返回监听状态。
实际上,这种做法很脆弱。使用带缓冲的通道会有所帮助,但仍然可能压垮接收者。更好的做法是,对于你不想发送的数字,根本不要使用 select 语句,而是使用阻塞式发送来代替 select。
感谢 Jakob 的解释,我现在似乎更清楚了……
更多关于Golang中select语句的使用问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
所以,在
select {
case guard(n % 2 == 0, c) <- n:
default:
continue
}
这段代码中,你的意思是“如果此时有接收方准备好接收,就将 n 发送到这个通道,否则就直接继续”。如果 guard 函数返回 nil,那就意味着没有接收方准备好,我们确实会直接继续执行。如果 guard 函数返回一个通道,那么这个通道可能有接收方准备好了,但在你的情况下,接收方的 goroutine 很可能正忙于执行 fmt.Println,无法处理这次发送。在这种情况下,我们同样会跳过它。
你添加的延迟使得接收方的 goroutine 更有可能有时间完成打印并返回监听状态。
实际上,这种做法是脆弱的。使用带缓冲的通道会有所帮助,但仍然可能压垮接收方。更好的做法是,对于你不想发送的数字,根本就不要执行这个 select 语句,并且使用阻塞式发送来代替 select。
在Go语言中,nil通道在select语句中的行为是:对nil通道的发送或接收操作会被阻塞,因此在select中永远不会被选中。你的代码问题在于guard函数返回的通道在select语句中被重新计算,但select的case表达式在进入select时只会被求值一次。
以下是关键点分析:
-
select的求值时机:select语句中的所有通道表达式在进入select时被求值。这意味着guard(n % 2 == 0, c)在每次循环中只被调用一次,其结果通道用于整个select操作。 -
问题根源:当
n为奇数时,guard返回nil通道,case中的发送操作被阻塞,因此default分支执行。但当n变为偶数时,guard返回通道c,而c可能尚未准备好接收(因为消费者可能处理较慢)。此时如果没有default,select会阻塞直到c可写;但因为有default,它会立即执行continue,跳过发送。 -
延迟的作用:
time.Sleep给了消费者(main中的range循环)时间从通道c接收数据,从而让c在下一次迭代时处于可写状态。
以下是修改后的代码,使用两个case分别处理偶数和奇数情况,避免依赖default和延迟:
package main
import (
"fmt"
)
func GenInts() <-chan int {
c := make(chan int)
go func() {
defer close(c)
for i := 0; i < 10; i++ {
c <- i
}
}()
return c
}
func EvenCheck(ints <-chan int) <-chan int {
c := make(chan int)
go func() {
defer close(c)
for n := range ints {
// 使用两个case:一个处理偶数发送,一个处理奇数跳过
select {
case c <- n: // 只有当c可写且n为偶数时才发送
// 这里依赖外部条件判断,但select本身不区分奇偶
default:
// 如果c不可写,跳过当前n
}
}
}()
return c
}
func main() {
for n := range EvenCheck(GenInts()) {
fmt.Println(n)
}
}
但上述代码仍无法区分奇偶数。正确做法是在select外判断奇偶,然后决定发送到通道或跳过:
func EvenCheck(ints <-chan int) <-chan int {
c := make(chan int)
go func() {
defer close(c)
for n := range ints {
if n%2 == 0 {
// 只发送偶数
c <- n
}
}
}()
return c
}
如果你坚持使用select和nil通道的模式,可以这样实现:
func EvenCheck(ints <-chan int) <-chan int {
c := make(chan int)
go func() {
defer close(c)
var sendChan chan<- int // 默认为nil
var sendValue int
for {
select {
case n, ok := <-ints:
if !ok {
return
}
if n%2 == 0 {
sendChan = c
sendValue = n
} else {
sendChan = nil
}
case sendChan <- sendValue:
sendChan = nil // 发送后重置为nil
}
}
}()
return c
}
这个版本使用单独的sendChan控制发送条件,符合nil通道阻塞的设计意图,且无需time.Sleep。

