Golang 标准库 tips -- select

Golang 的 select 语句的作用是用来监听多个 channel 的读写操作,当 channel 读写操作发生时,会触发对于的 case 执行。在实际使用过程中,有以下需要注意的地方。

for-select 循环退出

我们在普通的 for 循环中,如果想退出循环,可以使用 break 语法退出,想要忽略本地循环继续下一次迭代可以通过 continue 来控制,但是在 selet 的 for 循环中,continue 的作用作用还是忽略本地循环继续下次循环。但是 break 的作用却是跳出本次 select,继续下一次 for 循环。

func TestForSelect() {
    var i int
    ticker := time.NewTicker(1 * time.Second)
    for {
        select {
        case <-ticker.C:
            i++
            if i == 3 {
                continue
            }
        }
        fmt.Println(i)
    }
    fmt.Println("for循环外")
}

// 打印结果
1
2
4
5
...

func TestForSelect() {
    var i int
    ticker := time.NewTicker(1 * time.Second)
    for {
        select {
        case <-ticker.C:
            i++
            if i == 3 {
                break
            }
        }
        fmt.Println(i)
    }
    fmt.Println("for循环外")
}

// 打印结果
1
2
3
4
5
...

想要退出 for select 循环有以下方式:
通过 break label 的方式,注意 label 需要写在 for 循环的前面并且紧挨着 for 循环:

func TestForSelect() {
    var i int
    ticker := time.NewTicker(1 * time.Second)
Loop:
    for {
        select {
        case <-ticker.C:
            i++
            if i == 3 {
                break Loop
            }
        }
        fmt.Println(i)
    }
    fmt.Println("for循环外")
}

// 打印结果
1
2
for循环外

通过 goto + label 的方式跳转到指定的位置,此时 label 需要些在 for 循环后面。

func TestForSelect() {
    var i int
    ticker := time.NewTicker(1 * time.Second)
    for {
        select {
        case <-ticker.C:
            i++
            if i == 3 {
                goto Exit
            }
        }
        fmt.Println(i)
    }
Exit:
    fmt.Println("for循环外")
}

// 打印结果
1
2
for循环外

当然最直接的方法跳出循环是在 case 中调用 return 结束本次函数,这种做法适用在 for 循环之后没有其他的逻辑需要执行。

func TestForSelect() {
    var i int
    ticker := time.NewTicker(1 * time.Second)
    for {
        select {
        case <-ticker.C:
            i++
            if i == 3 {
                return
            }
        }
        fmt.Println(i)
    }
    fmt.Println("for循环外")
}

// 打印结果
1
2
select case 的执行顺序

我们知道普通的 switch case 的执行顺序是按照 case 从上到下执行,如果有一个 case 满足条件,则不执行后续的 case,否则如果都没有满足条件,如果有 default 的 case 则会执行 default 的 case。我们可以通过下面的这个例子可以看出程序从上到下判断到 case1 满足条件之后就执行 case1 的逻辑了。

func main() {

    switch {
    case left(0) == right(0):
        fmt.Println("case1 run")
    case left(1) == right(1):
        fmt.Println("case2 run")
    }
}

func left(i int) int {
    fmt.Println("left:", i)
    return i
}

func right(i int) int {
    fmt.Println("right:", i)
    return i
}

// 打印结果:
left: 0
right: 0
case1 run

这里也顺便说明下我们在通过 if 进行判断的时候如果一个 if 语句中有多个或语句,只要前面的满足了条件匹配到了是不会执行后面的条件判断了。

func main() {
    if left(0) == right(0) || left(1) == right(1) {
        fmt.Println("test")
    }
}

func left(i int) int {
    fmt.Println("left:", i)
    return i
}

func right(i int) int {
    fmt.Println("right:", i)
    return i
}

// 打印结果
left: 0
right: 0
test

然而我们通过 select case 的方式来执行是不一样的,这些 case 会按照从上到下,从左到右的顺序执行一遍,如果有多个 case 同时满足 channel 就绪条件,则随机的从其中选择一个 case 进行执行。通过下面的例子我们可以得到验证,多个 case 的条件都会执行一遍。

func main() {

    ch1 := make(chan int, 1)
    ch2 := make(chan int, 1)
    chs := []chan int{ch1, ch2}

    select {
    case left(0, chs) <- right(0):
        fmt.Println("case1 run")
    case left(1, chs) <- right(1):
        fmt.Println("case2 run")
    }
}

func left(i int, chs []chan int) chan int {
    fmt.Println("left:", i)
    return chs[i]
}

func right(i int) int {
    fmt.Println("right:", i)
    return i
}

// 打印结果:
left: 0
right: 0
left: 1
right: 1
case1 run // 这一行代码会随机打印 case1 run 和 case2 run

我们需要特别注意如果 select 的 case 是函数操作的情况下,会等到这些 case 的所有函数操作都全部返回的情况下再从满足 channel 就绪的 case 中随机选择。
我们先来看一个下面的例子,select 监听了 3 个 channel 的读操作,其中 case1 和 case2 调用之后都是立即返回 channel,不同的是 case1 开启了 groutine 在 50ms 之后往 channel 里面写数据,case2 在 200ms 之后往 channel 里面写入数据;而 case3 调用了 AsyncCall2 方法在 3000ms 之后返回可读的 channel。

func AsyncCall(t int) <-chan int {
    c := make(chan int)
    go func() {
        time.Sleep(time.Millisecond * time.Duration(t))
        c <- t
    }()
    return c
}

func AsyncCall2(t int) <-chan int {
    c := make(chan int)
    go func() {
        c <- t
    }()
    time.Sleep(3000 * time.Millisecond)
    return c
}

func main() {
    select {
    case resp := <-AsyncCall(50):
        fmt.Println(resp)
    case resp := <-AsyncCall(200):
        fmt.Println(resp)
    case resp := <-AsyncCall2(3000):
        fmt.Println(resp)
    }
}

// 打印结果
在等待 3000ms 之后随机在 502003000 中打印一个值

可能会有很多人认为上面的代码会在 50 ms 到的时候固定打印 50,因为觉得第一个 case 的执行是最快的,但是这种认知是错误的,正确的结果是程序会在等待 3000ms 之后随机的在 50、200、3000 中打印一个值。
原因是因为当 select 语句需要从多个 channel 中读取或写入时,会将所以的 case 表达式都执行一遍,包括了在case语句中的函数调用,会等待函数先全部返回,如果有 default 分支,则执行 default 分支语句,如果没有 default 分支,则select语句会一直阻塞,直到至少有一个通信操作可以进行。
所以上面的程序需要等待 case3 在 3000ms 返回之后才会进入到满足条件 case 的判断,此时 case1 、case2、case3 都已经满足可读的条件了,根据多个 case 都可读会随机选择一个 case 执行读操作的规则,最后的结果是在 50、200、3000 中随机打印一个值。
我们对上面的例子改造一下,如果 3 个case 都调用 AsyncCall 方法,那么程序会在 50ms 之后固定打印 50,因为 select 中的 3 个case 调用的方法都是立即就执行完成了,然后 case1 是最先就绪的。

func AsyncCall(t int) <-chan int {
    c := make(chan int)
    go func() {
        time.Sleep(time.Millisecond * time.Duration(t))
        c <- t
    }()
    return c
}

func main() {
    select {
    case resp := <-AsyncCall(50):
        fmt.Println(resp)
    case resp := <-AsyncCall(200):
        fmt.Println(resp)
    case resp := <-AsyncCall(3000):
        fmt.Println(resp)
    }
}

// 打印结果50ms 之后固定打印 50

或许有人会问当 select 中存在多个 case 满足执行条件,而又存在 default 语句的时候,因为 default 语句是永远满足可执行条件,那么会不会在随机的时候会有一定的机会执行 default 语句的 case,答案是不会的,default 语句只有当所有的非 default 语句都不满足条件的时候才会执行到。

func AsyncCall(t int) <-chan int {
    c := make(chan int, 1)
    go func() {
        time.Sleep(time.Millisecond * time.Duration(t))
        c <- t
    }()

    time.Sleep(100 * time.Millisecond)
    return c
}

func main() {

    select {
    case resp := <-AsyncCall(50):
        fmt.Println(resp)
    default:
        fmt.Println("default case run")
    }
}
// 打印结果
50

上面的例子 select 在执行的时候,case1 和 default 都满足执行条件,但是程序永远是先执行非 default 的 case。

select case 实现优先级

通过上面的分析我们已经知道了 select 在执行 case 语句的时候是不保障顺序执行,但是如果我们需要实现一个函数从 ch1 和 ch2 中接收任务,如果 ch1 和 ch2 同时都满足执行条件的时候,我们要保障 ch1 的任务的执行顺序要在 ch2 之前,也就是需要实现一个包含优先级的 case 执行顺序,这种情况是可以通过 defalut 来实现,为此写了第一版代码实现如下:

type Job func()

func main() {
    ch1 := make(chan Job, 10)
    ch2 := make(chan Job, 10)

    job1 := func() {
        fmt.Println("job1 run")
    }
    job2 := func() {
        fmt.Println("job2 run")
    }
    ch2 <- job2
    ch1 <- job1
    
    go func() {
        time.Sleep(100 * time.Second)
    }()

    PrioritySelect(ch1, ch2)
}

func PrioritySelect(ch1, ch2 chan Job) {
    for {
        select {
        case job1 := <-ch1:
            job1()
        default:
            select {
            case job2 := <-ch2:
                job2()
            }
        }
    }
}

// 运行结果:
job1 run
job2 run

从结果来分析我们确实是实现了 job1 在 job2 之前执行,其中的原因在上面的例子中也进行了分析,select 在执行的时候如果普通的 case 和 default 的 case 都满足执行条件的时候,会优先执行普通条件的 case,所以我们在第一次 for 循环的时候先执行了 case1,也就是 job1,然后第二次执行 for 循环的时候 case1 已经不满足执行条件了,会执行 default 中的语句,default 中也是调用一个 select 来监听 ch2 的读操作,所以第二次循环的时候会执行 job2。
除了使用 default 的用法之外,还有另外一种思路实现:

type Job func()

func main() {
    ch1 := make(chan Job, 10)
    ch2 := make(chan Job, 10)

    job1 := func() {
        fmt.Println("job1 run")
    }
    job2 := func() {
        fmt.Println("job2 run")
    }
    ch2 <- job2
    ch1 <- job1

    go func() {
        time.Sleep(100 * time.Second)
    }()

    PrioritySelect(ch1, ch2)
}

func PrioritySelect(ch1, ch2 chan Job) {
    for {
        select {
        case job1 := <-ch1:
            job1()
        case job2 := <-ch2:
        priority:
            for {
                select {
                case job1 := <-ch1:
                    job1()
                default:
                    break priority
                }
            }
            job2()
        }
    }
}
// 打印结果:
job1 run
job2 run

这种方法的实现思路是同时监听 ch1 和 ch2 的读操作,这样如果随机执行到 ch1 的操作,那么则优先执行 job1,如果执行到 ch2,我们在 case2 中再通过一个 for-select 监听 ch1,如果 ch1 满足条件,则执行 job1,随后执行 job2,然后进入下一次循环,否则如果 ch1 没有就绪,则退出第二层的 for 循环从而执行 job2 之后进入第一层 for 循环的下一次循环。

for-select 与 timer.After

time.After 主要用来做超时控制,其用法是传入一个超时时间,返回一个Time类型的阻塞 channel,在等待设置的超时时间到来之后会往这个 channel 中传入一个值,这样线程中监听到channel 的读操作之后再继续执行后面的方法。一般可以和 select 配合使用超时退出 groutine,但是 time.After 使用不当很容易引起内存泄露,我们一起来分析下。
我们注意到源码包中在 After 方法上有这么一段注释:计时器不会被垃圾回收直到计时器启动,也就是我们比如我们使用 time.After(60*time.Second),这个计时器会在 60s 之后才被垃圾回收。

// After waits for the duration to elapse and then sends the current time
// on the returned channel.
// It is equivalent to NewTimer(d).C.
// The underlying Timer is not recovered by the garbage collector
// until the timer fires. If efficiency is a concern, use NewTimer
// instead and call Timer.Stop if the timer is no longer needed.
func After(d Duration) <-chan Time {
    return NewTimer(d).C
}

下面这个例子使用 time.After 用来控制 run 函数的运行超时时间,run 函数在 100ms 就返回了,但是 time.After 超时时间是 3s,也就是在 100ms 到 3s 之间 timer 是不会被释放的,这个 timer 其实就泄露了。

type resp struct {
}

func run() <-chan resp {
    ch := make(chan resp)

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch <- resp{}
    }()

    return ch
}

func main() {
    select {
    case resp := <-run():
        fmt.Println("run success", resp)
    case <-time.After(3 * time.Second):
        fmt.Println("timeout")
    }
    time.Sleep(100 * time.Second) // prevent main exit
}

如果我们在 for-select 中使用 time.After,但是其他的 case 很快就会返回,那么我们将会创建非常多 timer 对象,并且在超时时间之前都得不到回收,这样将会消耗大量的内存,造成内存泄露。

type resp struct {
}

func run() <-chan resp {
    ch := make(chan resp)

    go func() {
        ch <- resp{}
    }()

    return ch
}

func main() {
    for {
        select {
        case resp := <-run(): // 很快就返回
            fmt.Println("run success", resp)
        case <-time.After(3 * time.Second): // 造成内存泄露
            fmt.Println("timeout")
        }
    }
}

因为 timer.After 我们不能主动取消,所以改进的地方是需要一个可以取消的 timer,正好 go 为我们提供了 NewTimer 和 NewTicker,我们可以调用 timer.Stop、ticker.Stop 方法主动取消 timer 或者 ticker。

type resp struct {
}

func run() <-chan resp {
    ch := make(chan resp)

    go func() {
        ch <- resp{}
    }()

    return ch
}

func main() {
    for {
        ticker := time.NewTimer(3 * time.Second)
        select {
        case resp := <-run():
            fmt.Println("run success", resp)
            ticker.Stop()
        case <-ticker.C:
            fmt.Println("timeout")
        }
    }
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值