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

3 回复

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语句中被重新计算,但selectcase表达式在进入select时只会被求值一次。

以下是关键点分析:

  1. select的求值时机select语句中的所有通道表达式在进入select时被求值。这意味着guard(n % 2 == 0, c)在每次循环中只被调用一次,其结果通道用于整个select操作。

  2. 问题根源:当n为奇数时,guard返回nil通道,case中的发送操作被阻塞,因此default分支执行。但当n变为偶数时,guard返回通道c,而c可能尚未准备好接收(因为消费者可能处理较慢)。此时如果没有defaultselect会阻塞直到c可写;但因为有default,它会立即执行continue,跳过发送。

  3. 延迟的作用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
}

如果你坚持使用selectnil通道的模式,可以这样实现:

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

回到顶部