select语句处于阻塞状态直到其中一个channel的收/发操作准备就绪,如果同时有多个channel的收/发操作准备就绪则随机选择其中一个。select最为常见的应用就是IO超时控制,例如HTTP客户端请求,建立TCP连接等等都使用了select来进行超时控制。
非阻塞的方式请见另一篇博客《Go语言:select的典型应用场景之IO超时控制的示例(采用default,非阻塞方式)》
阻塞方式示例:
请求超时设为50毫秒,模拟的IO操作响应延迟为0-100毫秒之间的随机数。多运行几次则部分有响应,其余为超时报错。
package main
import (
"errors"
"fmt"
"math/rand"
"time"
)
type Response struct {
body string //响应内容
elapse time.Duration //响应耗时
}
func request(timeout time.Duration) (Response, error) {
//用于触发超时的计时器
timer := time.NewTimer(timeout)
//用于接收IO响应的通道
respCh := make(chan string)
start := time.Now()
//另起goroutine执行IO操作
go doIO(respCh)
var respData Response
var err error
//如果先接收到响应则停掉超时计时器
//如果超时计时器先触发,则生成一个error的实例
select {
case respData.body = <-respCh:
timer.Stop()
case <-timer.C:
err = errors.New("request timeout")
}
//记录总耗时
respData.elapse = time.Now().Sub(start)
return respData, err
}
func doIO(respCh chan string) {
//随机产生一个[0,100)毫秒的延迟,以模拟IO延时延迟
rand.Seed(time.Now().UnixNano())
delay := time.Duration(rand.Intn(100)) * time.Millisecond
fmt.Printf("delay=%v\n", delay)
time.Sleep(delay)
respCh <- "Hello World"
}
func main() {
resp, err := request(50 * time.Millisecond)
if err != nil {
fmt.Printf("error: %s\n", err.Error())
return
}
fmt.Printf("response: body=%s elapse=%s\n", resp.body, resp.elapse)
}
输出:
[root@dev tutorial]# go run select.go
delay=13ms
response: body=Hello World elapse=13.231063ms
[root@dev tutorial]# go run select.go
delay=64ms
error: request timeout
[root@dev tutorial]# go run select.go
delay=67ms
error: request timeout
[root@dev tutorial]# go run select.go
delay=82ms
error: request timeout
[root@dev tutorial]# go run select.go
delay=35ms
response: body=Hello World elapse=35.285941ms
Go源码里的例子:
net/http/client.go中函数setRequestCancel的代码节选。这部分代码就是http客户端请求超时控制的核心部分。
go func() {
select {
case <-initialReqCancel:
doCancel()
timer.Stop()
case <-timer.C:
timedOut.setTrue()
doCancel()
case <-stopTimerCh:
timer.Stop()
}
}()
上面的示例也可以把timer改成context.WithDeadline()
func request(timeout time.Duration) (Response, error) {
//用于触发超时的上下文
deadline := time.Now().Add(timeout)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
//用于接收IO响应的通道
respCh := make(chan string)
start := time.Now()
//另起goroutine执行IO操作
go doIO(respCh)
var respData Response
var err error
//如果先接收到响应则停掉超时计时器
//如果超时计时器先触发,则生成一个error的实例
select {
case respData.body = <-respCh:
case <-ctx.Done():
err = errors.New("request timeout")
}
//记录总耗时
respData.elapse = time.Now().Sub(start)
return respData, err
}