Golang基础-Select

基本概念

  • select 是 Go 中的一个控制结构,类似于 switch 语句。
  • select 语句只能用于通道(channel)操作,每个 case 必须是一个通道操作,要么是发送要么是接收。
  • select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。
  • 如果多个通道都准备好,那么 select 语句会随机选择一个通道执行。如果所有通道都没有准备好,那么执行 default 块中的代码。

select监听一堆channel,哪个channel来数据了就执行相应的代码。来数据了就是就绪,就是有人往channel里写数据了。

例子

下面的代码模拟火箭发射倒计时程序,倒数10秒没人中断就发射,否则终止。终止的操作是按键盘的回车,用一个goroutine监听是否有这个操作。这个等待回车的goroutine实际上是阻塞了,直到输入回车才停止阻塞,继续执行下面的代码。
通过新建一个名为abort的channel来表示是否有人按下回车终止发射。一旦有人按下回车,上面说的那个等待输入的处于阻塞状态的goroutine就继续执行下一行代码,也就是往abort这个channel里写数据。(注意这里的空结构体作用)abort一旦数据就绪,可以被读取,那么select就有机会选择相应的代码块执行,这部分代码就是终止发射。
注意ticker.C也是一个channel,每隔一秒往里面写一条数据。for循环每隔一秒打印一次倒数数据,为什么是一秒?因为select的两个case都是阻塞的,ticker.C等待一秒变得就绪,abort则是有人输入回车才会就绪。所以最终结果就是,如果一直没人输入回车,这个select只有等ticker等一秒变得就绪才能继续执行。所以每次for循环都会阻塞一秒钟。
ticker最后要stop,否则一直往channel里写数据。goroutine泄露?可以想象成这个ticker也是一个goroutine,如果不stop,那么他一直存活,尽管我们的程序已经不需要用他了,但他还是默默无闻地往channel里写数据。

package main

import (
	"fmt"
	"os"
	"time"
)

func launch() {
	fmt.Println("Launching rockets")
}

func main() {
	abort := make(chan struct{})
	go func() {
		os.Stdin.Read(make([]byte, 1))
		abort <- struct{}{}
	}()

	fmt.Println("Commencing countdown.  Press return to abort.")
	ticker := time.NewTicker(1 * time.Second)
	for countdown := 10; countdown > 0; countdown-- {
		fmt.Println(countdown)
		select {
		case <-ticker.C: // receive from the ticker's channel
			// Do nothing.
		case <-abort:
			fmt.Println("Launch aborted!")
			return
		}
	}
	ticker.Stop() // cause the ticker's goroutine to terminate
	launch()
}

下面是另一个例子。这个例子演示了生产者消费者模式。通过done channel来通知select结束监听。这里是直接close(done),因为读一个已经关闭的channel,会读到0值,如果还写了ok的话,会读到false。其实这么写我觉得不太好,还不如往done里写一条数据容易理解。
done这个channel,如果没有外部干扰,是一直处于非就绪的状态,没有东西可以读。但是,一旦将他关闭,我们就能读到相应的0值。我们这里只关心能不能读,不关系读出来什么,所以用close能实现一样的效果。

func main() {
    messages := make(chan int, 10)
    done := make(chan bool)

    defer close(messages)
    // consumer
    go func() {
        ticker := time.NewTicker(1 * time.Second)
        for _ = range ticker.C {
            select {
            case <-done:
                fmt.Println("child process interrupt...")
                return
            default:
                fmt.Printf("send message: %d\n", <-messages)
            }
        }
    }()

    // producer
    for i := 0; i < 10; i++ {
        messages <- i
    }
    time.Sleep(5 * time.Second)
    close(done)
    time.Sleep(1 * time.Second)
    fmt.Println("main process exit!")
}

Context

将上面的倒数发射程序改为使用context实现。
context作用:

  • 上层任务取消后,所有的下层任务都会被取消;
  • 中间某一层的任务取消后,只会将当前任务的下层任务取消,而不会影响上层的任务以及同级任务。
package main

import (
	"context"
	"fmt"
	"os"
	"time"
)

func launch() {
	fmt.Println("Launching rockets")
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		os.Stdin.Read(make([]byte, 1))
		cancel()
	}()

	fmt.Println("Commencing countdown.  Press return to abort.")
	ticker := time.NewTicker(1 * time.Second)
	for countdown := 10; countdown > 0; countdown-- {
		fmt.Println(countdown)
		select {
		case <-ticker.C: // receive from the ticker's channel
			// Do nothing.
		case <-ctx.Done():
			fmt.Println("Launch aborted!")
			return
		}
	}
	ticker.Stop() // cause the ticker's goroutine to terminate
	launch()
}

将context想象成一棵树,一停停一串。一个请求超时了,基于该请求的所有操作都停止,防止浪费资源。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

参考

深入理解Golang之context
go context详解 - 牛奔 - 博客园

posted @ 2023-04-05 22:42  roadwide  阅读(341)  评论(0编辑  收藏  举报